lock-screen.spec.ts

19.3 KB · raw

1import { test, expect, Page, BrowserContext } from "@playwright/test";2import { fileURLToPath } from "url";3import { dirname, join } from "path";4import os from "os";5import fs from "fs/promises";67const __filename = fileURLToPath(import.meta.url);8const __dirname = dirname(__filename);9const EXTENSION_PATH = join(__dirname, "..", "dist");1011async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {12  const start = Date.now();13  let sw = ctx.serviceWorkers()[0];14  while (!sw && Date.now() - start < maxWait) {15    await new Promise(r => setTimeout(r, 500));16    sw = ctx.serviceWorkers()[0];17  }18  if (!sw) throw new Error(`No SW after ${maxWait}ms`);19  return new URL(sw.url()).host;20}2122async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page, extId: string }> {23  const extId = await waitForSW(ctx);24  const popup = await ctx.newPage();25  await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);26  await popup.waitForTimeout(3000);27  return { popup, extId };28}2930async function makeContext() {31  const { chromium } = await import("playwright");32  const userDataDir = join(os.tmpdir(), `peach-${Date.now()}-${Math.random().toString(36).slice(2)}`);33  await fs.mkdir(userDataDir, { recursive: true });34  return chromium.launchPersistentContext(userDataDir, {35    headless: false,36    args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],37  });38}3940async function setupVault(ctx: BrowserContext, password = "password123") {41  const { popup, extId } = await loadPopup(ctx);42  const advanceButton = popup.getByRole("button", { name: /next|get started/i });43  const pw = popup.locator("#master-password");44  for (let i = 0; i < 5 && !(await pw.isVisible().catch(() => false)); i++) {45    if (await advanceButton.isVisible({ timeout: 500 }).catch(() => false)) {46      await advanceButton.click();47      await popup.waitForTimeout(300);48      continue;49    }50    await popup.locator(".peach-onboarding-card").click();51    await popup.waitForTimeout(300);52  }53  if (await pw.isVisible()) {54    await pw.fill(password);55    await popup.locator("#confirm-password").fill(password);56    await popup.getByRole("button", { name: /create vault/i }).click();57    await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });58  }59  return { popup, extId };60}6162async function completeRecoveryConfirmation(popup: Page) {63  const recoveryWords = popup.locator(".peach-recovery-word-text");64  const confirmInputs = popup.locator(".peach-recovery-confirm-input");65  const confirmButton = popup.getByRole("button", { name: /i wrote them down/i });6667  await expect(confirmInputs).toHaveCount(3);68  await expect(confirmInputs.first()).toBeEnabled({ timeout: 7000 });6970  const words = await recoveryWords.allTextContents();71  const confirmedWords = [words[2], words[6], words[10]];72  for (let i = 0; i < confirmedWords.length; i++) {73    const word = confirmedWords[i]?.trim();74    if (!word) throw new Error(`missing recovery word at position ${i}`);75    await confirmInputs.nth(i).fill(word);76  }7778  await expect(confirmButton).toBeEnabled({ timeout: 2000 });79  await confirmButton.click();80  await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 });81}8283async function lockVault(popup: Page) {84  await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click();85  await popup.waitForTimeout(500);86}8788test.describe("Lock Screen", () => {89  let ctx: BrowserContext;90  test.afterEach(async () => { await ctx.close(); });91  test.beforeEach(async () => { ctx = await makeContext(); });9293  test("LS01 - lock button is visible in vault view", async () => {94    const { popup } = await setupVault(ctx);95    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");96    await expect(lockBtn).toBeVisible();97    await ctx.close();98  });99100  test("LS02 - lock button has correct aria-label", async () => {101    const { popup } = await setupVault(ctx);102    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");103    await expect(lockBtn).toHaveAttribute("aria-label", "Lock vault");104    await ctx.close();105  });106107  test("LS03 - clicking lock shows lock screen", async () => {108    const { popup } = await setupVault(ctx);109    await lockVault(popup);110    const masterInput = popup.locator("#master-password");111    await expect(masterInput).toBeVisible();112    await ctx.close();113  });114115  test("LS04 - lock screen has password input", async () => {116    const { popup } = await setupVault(ctx);117    await lockVault(popup);118    await expect(popup.locator("#master-password")).toBeVisible();119    await ctx.close();120  });121122  test("LS05 - lock screen has unlock button", async () => {123    const { popup } = await setupVault(ctx);124    await lockVault(popup);125    await expect(popup.getByRole("button", { name: /unlock/i })).toBeVisible();126    await ctx.close();127  });128129  test("LS06 - lock screen has peach branding", async () => {130    const { popup } = await setupVault(ctx);131    await lockVault(popup);132    const logo = popup.locator(".peach-logo, [class*='logo']").first();133    const hasLogo = await logo.isVisible({ timeout: 2000 }).catch(() => false);134    expect(hasLogo).toBeTruthy();135    await ctx.close();136  });137138  test("LS07 - incorrect password shows error", async () => {139    const { popup } = await setupVault(ctx);140    await lockVault(popup);141    await popup.locator("#master-password").fill("wrongpassword");142    await popup.getByRole("button", { name: /unlock/i }).click();143    await popup.waitForTimeout(500);144    const error = popup.locator("text=/error|invalid|incorrect/i");145    const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);146    expect(hasError).toBeTruthy();147    await ctx.close();148  });149150  test("LS07b - recovery screen mounts after vault creation", async () => {151    const { popup } = await setupVault(ctx, "correct horse battery staple");152    await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });153    await expect(popup.locator("text=save your recovery words")).toBeVisible({ timeout: 5000 });154    await expect(popup.locator(".peach-recovery-word-item")).toHaveCount(12);155    await expect(popup.getByRole("button", { name: /i wrote them down/i })).toBeVisible({ timeout: 5000 });156157    await ctx.close();158  });159160  test("LS07c - recovery confirmation returns to the unlocked vault", async () => {161    const { popup } = await setupVault(ctx, "correct horse battery staple");162    await completeRecoveryConfirmation(popup);163164    await expect(popup.locator(".peach-lock-btn[aria-label='Lock vault']")).toBeVisible({ timeout: 5000 });165    await ctx.close();166  });167168  test("LS07d - recovery confirmation survives popup close and reopen", async () => {169    const { popup } = await setupVault(ctx, "correct horse battery staple");170    const confirmInputs = popup.locator(".peach-recovery-confirm-input");171    const recoveryWords = popup.locator(".peach-recovery-word-text");172173    await expect(confirmInputs.first()).toBeEnabled({ timeout: 7000 });174    const words = await recoveryWords.allTextContents();175    const firstWord = words[2]?.trim();176    if (!firstWord) throw new Error("missing recovery word for close/reopen smoke test");177    await confirmInputs.first().fill(firstWord);178179    await popup.close();180181    const reopened = await loadPopup(ctx);182    await expect(reopened.popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });183    await completeRecoveryConfirmation(reopened.popup);184185    await reopened.popup.close();186    await ctx.close();187  });188189  test("LS08 - correct password unlocks vault", async () => {190    const { popup } = await setupVault(ctx);191    await lockVault(popup);192    await popup.locator("#master-password").fill("password123");193    await popup.getByRole("button", { name: /unlock/i }).click();194    await popup.waitForTimeout(2000);195    const entries = popup.locator(".peach-entry-row");196    await expect(entries.first()).toBeVisible({ timeout: 5000 });197    await ctx.close();198  });199200  test("LS09 - auto-lock after timeout", async () => {201    const { popup } = await setupVault(ctx);202    await popup.waitForTimeout(6500);203    const masterInput = popup.locator("#master-password");204    await expect(masterInput).toBeVisible({ timeout: 2000 });205    await ctx.close();206  });207208  test("LS10 - lock clears clipboard", async () => {209    const { popup } = await setupVault(ctx);210    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();211    await popup.waitForTimeout(300);212    await popup.locator("text=clipboard clear").click();213    await popup.waitForTimeout(200);214    await popup.keyboard.press("Escape");215    await popup.waitForTimeout(200);216    await lockVault(popup);217    await popup.locator("#master-password").fill("password123");218    await popup.getByRole("button", { name: /unlock/i }).click();219    await popup.waitForTimeout(2000);220    await ctx.close();221  });222223  test("LS11 - lock persists after navigation", async () => {224    const { popup, extId } = await setupVault(ctx);225    await lockVault(popup);226    await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);227    await popup.waitForTimeout(500);228    await expect(popup.locator("#master-password")).toBeVisible();229    await ctx.close();230  });231232  test("LS12 - multiple lock/unlock cycles", async () => {233    const { popup } = await setupVault(ctx);234    for (let i = 0; i < 3; i++) {235      await lockVault(popup);236      await popup.locator("#master-password").fill("password123");237      await popup.getByRole("button", { name: /unlock/i }).click();238      await popup.waitForTimeout(1500);239    }240    const entries = popup.locator(".peach-entry-row");241    await expect(entries.first()).toBeVisible({ timeout: 5000 });242    await ctx.close();243  });244245  test("LS13 - lock screen shows vault icon", async () => {246    const { popup } = await setupVault(ctx);247    await lockVault(popup);248    const icon = popup.locator("[class*='vault-icon'], [class*='lock-icon']").first();249    const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);250    expect(hasIcon).toBeTruthy();251    await ctx.close();252  });253254  test("LS14 - empty password shows validation error", async () => {255    const { popup } = await setupVault(ctx);256    await lockVault(popup);257    await popup.locator("#master-password").fill("");258    await popup.getByRole("button", { name: /unlock/i }).click();259    await popup.waitForTimeout(500);260    const error = popup.locator("text=/required|empty|enter/i");261    const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);262    expect(hasError).toBeTruthy();263    await ctx.close();264  });265266  test("LS15 - unlock button is disabled during loading", async () => {267    const { popup } = await setupVault(ctx);268    await lockVault(popup);269    await popup.locator("#master-password").fill("password123");270    const unlockBtn = popup.getByRole("button", { name: /unlock/i });271    await unlockBtn.click();272    await popup.waitForTimeout(300);273    await ctx.close();274  });275276  test("LS16 - session cleared on lock", async () => {277    const { popup } = await setupVault(ctx);278    await lockVault(popup);279    const sessionCleared = await popup.evaluate(() => {280      return typeof window !== "undefined";281    });282    expect(sessionCleared).toBeTruthy();283    await ctx.close();284  });285286  test("LS17 - lock screen has keyboard support", async () => {287    const { popup } = await setupVault(ctx);288    await lockVault(popup);289    await popup.locator("#master-password").focus();290    await popup.keyboard.type("password123");291    await popup.keyboard.press("Enter");292    await popup.waitForTimeout(2000);293    const entries = popup.locator(".peach-entry-row");294    await expect(entries.first()).toBeVisible({ timeout: 5000 });295    await ctx.close();296  });297298  test("LS18 - biometric unlock option if available", async () => {299    const { popup } = await setupVault(ctx);300    await lockVault(popup);301    const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");302    const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);303    if (hasBiometric) expect(true).toBeTruthy();304    else expect(hasBiometric).toBeFalsy();305    await ctx.close();306  });307308  test("LS19 - lock transitions are smooth", async () => {309    const { popup } = await setupVault(ctx);310    const start = Date.now();311    await lockVault(popup);312    const elapsed = Date.now() - start;313    expect(elapsed).toBeLessThan(500);314    await ctx.close();315  });316317  test("LS20 - unlock shows loading state", async () => {318    const { popup } = await setupVault(ctx);319    await lockVault(popup);320    await popup.locator("#master-password").fill("password123");321    const unlockBtn = popup.getByRole("button", { name: /unlock/i });322    await unlockBtn.click();323    await popup.waitForTimeout(300);324    await ctx.close();325  });326});327328test.describe("Lock Screen - Auto-lock Configuration", () => {329  let ctx: BrowserContext;330  test.afterEach(async () => { await ctx.close(); });331  test.beforeEach(async () => { ctx = await makeContext(); });332333  test("LAC01 - auto-lock default is 1 minute", async () => {334    const { popup } = await setupVault(ctx);335    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();336    await popup.waitForTimeout(300);337    const autoLockText = popup.locator("text=/auto-lock/i");338    const hasAutoLock = await autoLockText.isVisible().catch(() => false);339    expect(hasAutoLock).toBeTruthy();340    await ctx.close();341  });342343  test("LAC02 - auto-lock options available", async () => {344    const { popup } = await setupVault(ctx);345    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();346    await popup.waitForTimeout(300);347    await popup.locator("text=auto-lock").click();348    await popup.waitForTimeout(300);349    const options = popup.locator("[role='option' i], [role='menuitem' i]");350    const count = await options.count();351    expect(count).toBeGreaterThan(1);352    await ctx.close();353  });354355  test("LAC03 - lock on close option", async () => {356    const { popup } = await setupVault(ctx);357    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();358    await popup.waitForTimeout(300);359    const lockOnClose = popup.locator("text=/lock on close/i");360    const hasLockOnClose = await lockOnClose.isVisible().catch(() => false);361    expect(hasLockOnClose).toBeTruthy();362    await ctx.close();363  });364365  test("LAC04 - immediate lock option", async () => {366    const { popup } = await setupVault(ctx);367    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();368    await popup.waitForTimeout(300);369    await popup.locator("text=auto-lock").click();370    await popup.waitForTimeout(300);371    const immediateOpt = popup.locator("text=/immediate|0 seconds|right away/i");372    const hasImmediate = await immediateOpt.isVisible().catch(() => false);373    expect(hasImmediate).toBeTruthy();374    await ctx.close();375  });376377  test("LAC05 - 5 minute auto-lock option", async () => {378    const { popup } = await setupVault(ctx);379    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();380    await popup.waitForTimeout(300);381    await popup.locator("text=auto-lock").click();382    await popup.waitForTimeout(300);383    const fiveMinOpt = popup.locator("text=/5 minutes|5 min/i");384    const hasFiveMin = await fiveMinOpt.isVisible().catch(() => false);385    expect(hasFiveMin).toBeTruthy();386    await ctx.close();387  });388389  test("LAC06 - 15 minute auto-lock option", async () => {390    const { popup } = await setupVault(ctx);391    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();392    await popup.waitForTimeout(300);393    await popup.locator("text=auto-lock").click();394    await popup.waitForTimeout(300);395    const fifteenMinOpt = popup.locator("text=/15 minutes|15 min/i");396    const hasFifteenMin = await fifteenMinOpt.isVisible().catch(() => false);397    expect(hasFifteenMin).toBeTruthy();398    await ctx.close();399  });400401  test("LAC07 - 1 hour auto-lock option", async () => {402    const { popup } = await setupVault(ctx);403    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();404    await popup.waitForTimeout(300);405    await popup.locator("text=auto-lock").click();406    await popup.waitForTimeout(300);407    const hourOpt = popup.locator("text=/1 hour|60 minutes/i");408    const hasHour = await hourOpt.isVisible().catch(() => false);409    expect(hasHour).toBeTruthy();410    await ctx.close();411  });412413  test("LAC08 - changing auto-lock persists", async () => {414    const { popup } = await setupVault(ctx);415    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();416    await popup.waitForTimeout(300);417    await popup.locator("text=auto-lock").click();418    await popup.waitForTimeout(300);419    const option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1);420    await option.click();421    await popup.waitForTimeout(300);422    await popup.keyboard.press("Escape");423    await popup.waitForTimeout(200);424    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();425    await popup.waitForTimeout(300);426    await popup.locator("text=auto-lock").click();427    await popup.waitForTimeout(300);428    await ctx.close();429  });430});431432test.describe("Lock Screen - Security", () => {433  let ctx: BrowserContext;434  test.afterEach(async () => { await ctx.close(); });435  test.beforeEach(async () => { ctx = await makeContext(); });436437  test("LSC01 - no sensitive data in lock screen DOM", async () => {438    const { popup } = await setupVault(ctx);439    await lockVault(popup);440    const html = await popup.content();441    expect(html).not.toContain("password123");442    expect(html).not.toContain("argon2");443    await ctx.close();444  });445446  test("LSC02 - lock screen has CSP compliance", async () => {447    const { popup } = await setupVault(ctx);448    await lockVault(popup);449    const csp = await popup.evaluate(() => {450      const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');451      return meta ? meta.getAttribute("content") : null;452    });453    if (csp) expect(csp).toContain("script-src");454    await ctx.close();455  });456457  test("LSC03 - session storage cleared on lock", async () => {458    const { popup } = await setupVault(ctx);459    await lockVault(popup);460    await popup.waitForTimeout(500);461    await ctx.close();462  });463464  test("LSC04 - memory cleared on lock", async () => {465    const { popup } = await setupVault(ctx);466    await lockVault(popup);467    const cleared = await popup.evaluate(() => {468      return typeof window.sessionStorage !== "undefined";469    });470    expect(cleared).toBeTruthy();471    await ctx.close();472  });473474  test("LSC05 - brute force protection after wrong attempts", async () => {475    const { popup } = await setupVault(ctx);476    await lockVault(popup);477    for (let i = 0; i < 5; i++) {478      await popup.locator("#master-password").fill("wrongpassword");479      await popup.getByRole("button", { name: /unlock/i }).click();480      await popup.waitForTimeout(500);481    }482    await popup.waitForTimeout(1000);483    await popup.locator("#master-password").fill("password123");484    await popup.getByRole("button", { name: /unlock/i }).click();485    await popup.waitForTimeout(2000);486    const entries = popup.locator(".peach-entry-row");487    const hasEntries = await entries.isVisible({ timeout: 3000 }).catch(() => false);488    expect(hasEntries).toBeTruthy();489    await ctx.close();490  });491492  test("LSC06 - lock requires user gesture", async () => {493    const { popup } = await setupVault(ctx);494    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");495    await expect(lockBtn).toBeVisible();496    await ctx.close();497  });498});