lock-screen.spec.ts

18.4 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("LS08 - correct password unlocks vault", async () => {169    const { popup } = await setupVault(ctx);170    await lockVault(popup);171    await popup.locator("#master-password").fill("password123");172    await popup.getByRole("button", { name: /unlock/i }).click();173    await popup.waitForTimeout(2000);174    const entries = popup.locator(".peach-entry-row");175    await expect(entries.first()).toBeVisible({ timeout: 5000 });176    await ctx.close();177  });178179  test("LS09 - auto-lock after timeout", async () => {180    const { popup } = await setupVault(ctx);181    await popup.waitForTimeout(6500);182    const masterInput = popup.locator("#master-password");183    await expect(masterInput).toBeVisible({ timeout: 2000 });184    await ctx.close();185  });186187  test("LS10 - lock clears clipboard", async () => {188    const { popup } = await setupVault(ctx);189    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();190    await popup.waitForTimeout(300);191    await popup.locator("text=clipboard clear").click();192    await popup.waitForTimeout(200);193    await popup.keyboard.press("Escape");194    await popup.waitForTimeout(200);195    await lockVault(popup);196    await popup.locator("#master-password").fill("password123");197    await popup.getByRole("button", { name: /unlock/i }).click();198    await popup.waitForTimeout(2000);199    await ctx.close();200  });201202  test("LS11 - lock persists after navigation", async () => {203    const { popup, extId } = await setupVault(ctx);204    await lockVault(popup);205    await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);206    await popup.waitForTimeout(500);207    await expect(popup.locator("#master-password")).toBeVisible();208    await ctx.close();209  });210211  test("LS12 - multiple lock/unlock cycles", async () => {212    const { popup } = await setupVault(ctx);213    for (let i = 0; i < 3; i++) {214      await lockVault(popup);215      await popup.locator("#master-password").fill("password123");216      await popup.getByRole("button", { name: /unlock/i }).click();217      await popup.waitForTimeout(1500);218    }219    const entries = popup.locator(".peach-entry-row");220    await expect(entries.first()).toBeVisible({ timeout: 5000 });221    await ctx.close();222  });223224  test("LS13 - lock screen shows vault icon", async () => {225    const { popup } = await setupVault(ctx);226    await lockVault(popup);227    const icon = popup.locator("[class*='vault-icon'], [class*='lock-icon']").first();228    const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);229    expect(hasIcon).toBeTruthy();230    await ctx.close();231  });232233  test("LS14 - empty password shows validation error", async () => {234    const { popup } = await setupVault(ctx);235    await lockVault(popup);236    await popup.locator("#master-password").fill("");237    await popup.getByRole("button", { name: /unlock/i }).click();238    await popup.waitForTimeout(500);239    const error = popup.locator("text=/required|empty|enter/i");240    const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);241    expect(hasError).toBeTruthy();242    await ctx.close();243  });244245  test("LS15 - unlock button is disabled during loading", async () => {246    const { popup } = await setupVault(ctx);247    await lockVault(popup);248    await popup.locator("#master-password").fill("password123");249    const unlockBtn = popup.getByRole("button", { name: /unlock/i });250    await unlockBtn.click();251    await popup.waitForTimeout(300);252    await ctx.close();253  });254255  test("LS16 - session cleared on lock", async () => {256    const { popup } = await setupVault(ctx);257    await lockVault(popup);258    const sessionCleared = await popup.evaluate(() => {259      return typeof window !== "undefined";260    });261    expect(sessionCleared).toBeTruthy();262    await ctx.close();263  });264265  test("LS17 - lock screen has keyboard support", async () => {266    const { popup } = await setupVault(ctx);267    await lockVault(popup);268    await popup.locator("#master-password").focus();269    await popup.keyboard.type("password123");270    await popup.keyboard.press("Enter");271    await popup.waitForTimeout(2000);272    const entries = popup.locator(".peach-entry-row");273    await expect(entries.first()).toBeVisible({ timeout: 5000 });274    await ctx.close();275  });276277  test("LS18 - biometric unlock option if available", async () => {278    const { popup } = await setupVault(ctx);279    await lockVault(popup);280    const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");281    const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);282    if (hasBiometric) expect(true).toBeTruthy();283    else expect(hasBiometric).toBeFalsy();284    await ctx.close();285  });286287  test("LS19 - lock transitions are smooth", async () => {288    const { popup } = await setupVault(ctx);289    const start = Date.now();290    await lockVault(popup);291    const elapsed = Date.now() - start;292    expect(elapsed).toBeLessThan(500);293    await ctx.close();294  });295296  test("LS20 - unlock shows loading state", async () => {297    const { popup } = await setupVault(ctx);298    await lockVault(popup);299    await popup.locator("#master-password").fill("password123");300    const unlockBtn = popup.getByRole("button", { name: /unlock/i });301    await unlockBtn.click();302    await popup.waitForTimeout(300);303    await ctx.close();304  });305});306307test.describe("Lock Screen - Auto-lock Configuration", () => {308  let ctx: BrowserContext;309  test.afterEach(async () => { await ctx.close(); });310  test.beforeEach(async () => { ctx = await makeContext(); });311312  test("LAC01 - auto-lock default is 1 minute", async () => {313    const { popup } = await setupVault(ctx);314    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();315    await popup.waitForTimeout(300);316    const autoLockText = popup.locator("text=/auto-lock/i");317    const hasAutoLock = await autoLockText.isVisible().catch(() => false);318    expect(hasAutoLock).toBeTruthy();319    await ctx.close();320  });321322  test("LAC02 - auto-lock options available", async () => {323    const { popup } = await setupVault(ctx);324    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();325    await popup.waitForTimeout(300);326    await popup.locator("text=auto-lock").click();327    await popup.waitForTimeout(300);328    const options = popup.locator("[role='option' i], [role='menuitem' i]");329    const count = await options.count();330    expect(count).toBeGreaterThan(1);331    await ctx.close();332  });333334  test("LAC03 - lock on close option", async () => {335    const { popup } = await setupVault(ctx);336    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();337    await popup.waitForTimeout(300);338    const lockOnClose = popup.locator("text=/lock on close/i");339    const hasLockOnClose = await lockOnClose.isVisible().catch(() => false);340    expect(hasLockOnClose).toBeTruthy();341    await ctx.close();342  });343344  test("LAC04 - immediate lock option", async () => {345    const { popup } = await setupVault(ctx);346    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();347    await popup.waitForTimeout(300);348    await popup.locator("text=auto-lock").click();349    await popup.waitForTimeout(300);350    const immediateOpt = popup.locator("text=/immediate|0 seconds|right away/i");351    const hasImmediate = await immediateOpt.isVisible().catch(() => false);352    expect(hasImmediate).toBeTruthy();353    await ctx.close();354  });355356  test("LAC05 - 5 minute auto-lock option", async () => {357    const { popup } = await setupVault(ctx);358    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();359    await popup.waitForTimeout(300);360    await popup.locator("text=auto-lock").click();361    await popup.waitForTimeout(300);362    const fiveMinOpt = popup.locator("text=/5 minutes|5 min/i");363    const hasFiveMin = await fiveMinOpt.isVisible().catch(() => false);364    expect(hasFiveMin).toBeTruthy();365    await ctx.close();366  });367368  test("LAC06 - 15 minute auto-lock option", async () => {369    const { popup } = await setupVault(ctx);370    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();371    await popup.waitForTimeout(300);372    await popup.locator("text=auto-lock").click();373    await popup.waitForTimeout(300);374    const fifteenMinOpt = popup.locator("text=/15 minutes|15 min/i");375    const hasFifteenMin = await fifteenMinOpt.isVisible().catch(() => false);376    expect(hasFifteenMin).toBeTruthy();377    await ctx.close();378  });379380  test("LAC07 - 1 hour auto-lock option", async () => {381    const { popup } = await setupVault(ctx);382    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();383    await popup.waitForTimeout(300);384    await popup.locator("text=auto-lock").click();385    await popup.waitForTimeout(300);386    const hourOpt = popup.locator("text=/1 hour|60 minutes/i");387    const hasHour = await hourOpt.isVisible().catch(() => false);388    expect(hasHour).toBeTruthy();389    await ctx.close();390  });391392  test("LAC08 - changing auto-lock persists", async () => {393    const { popup } = await setupVault(ctx);394    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();395    await popup.waitForTimeout(300);396    await popup.locator("text=auto-lock").click();397    await popup.waitForTimeout(300);398    const option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1);399    await option.click();400    await popup.waitForTimeout(300);401    await popup.keyboard.press("Escape");402    await popup.waitForTimeout(200);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    await ctx.close();408  });409});410411test.describe("Lock Screen - Security", () => {412  let ctx: BrowserContext;413  test.afterEach(async () => { await ctx.close(); });414  test.beforeEach(async () => { ctx = await makeContext(); });415416  test("LSC01 - no sensitive data in lock screen DOM", async () => {417    const { popup } = await setupVault(ctx);418    await lockVault(popup);419    const html = await popup.content();420    expect(html).not.toContain("password123");421    expect(html).not.toContain("argon2");422    await ctx.close();423  });424425  test("LSC02 - lock screen has CSP compliance", async () => {426    const { popup } = await setupVault(ctx);427    await lockVault(popup);428    const csp = await popup.evaluate(() => {429      const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');430      return meta ? meta.getAttribute("content") : null;431    });432    if (csp) expect(csp).toContain("script-src");433    await ctx.close();434  });435436  test("LSC03 - session storage cleared on lock", async () => {437    const { popup } = await setupVault(ctx);438    await lockVault(popup);439    await popup.waitForTimeout(500);440    await ctx.close();441  });442443  test("LSC04 - memory cleared on lock", async () => {444    const { popup } = await setupVault(ctx);445    await lockVault(popup);446    const cleared = await popup.evaluate(() => {447      return typeof window.sessionStorage !== "undefined";448    });449    expect(cleared).toBeTruthy();450    await ctx.close();451  });452453  test("LSC05 - brute force protection after wrong attempts", async () => {454    const { popup } = await setupVault(ctx);455    await lockVault(popup);456    for (let i = 0; i < 5; i++) {457      await popup.locator("#master-password").fill("wrongpassword");458      await popup.getByRole("button", { name: /unlock/i }).click();459      await popup.waitForTimeout(500);460    }461    await popup.waitForTimeout(1000);462    await popup.locator("#master-password").fill("password123");463    await popup.getByRole("button", { name: /unlock/i }).click();464    await popup.waitForTimeout(2000);465    const entries = popup.locator(".peach-entry-row");466    const hasEntries = await entries.isVisible({ timeout: 3000 }).catch(() => false);467    expect(hasEntries).toBeTruthy();468    await ctx.close();469  });470471  test("LSC06 - lock requires user gesture", async () => {472    const { popup } = await setupVault(ctx);473    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");474    await expect(lockBtn).toBeVisible();475    await ctx.close();476  });477});