lock-screen.spec.ts

17.2 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 lockVault(popup: Page) {63  await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click();64  await popup.waitForTimeout(500);65}6667test.describe("Lock Screen", () => {68  let ctx: BrowserContext;69  test.afterEach(async () => { await ctx.close(); });70  test.beforeEach(async () => { ctx = await makeContext(); });7172  test("LS01 - lock button is visible in vault view", async () => {73    const { popup } = await setupVault(ctx);74    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");75    await expect(lockBtn).toBeVisible();76    await ctx.close();77  });7879  test("LS02 - lock button has correct aria-label", async () => {80    const { popup } = await setupVault(ctx);81    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");82    await expect(lockBtn).toHaveAttribute("aria-label", "Lock vault");83    await ctx.close();84  });8586  test("LS03 - clicking lock shows lock screen", async () => {87    const { popup } = await setupVault(ctx);88    await lockVault(popup);89    const masterInput = popup.locator("#master-password");90    await expect(masterInput).toBeVisible();91    await ctx.close();92  });9394  test("LS04 - lock screen has password input", async () => {95    const { popup } = await setupVault(ctx);96    await lockVault(popup);97    await expect(popup.locator("#master-password")).toBeVisible();98    await ctx.close();99  });100101  test("LS05 - lock screen has unlock button", async () => {102    const { popup } = await setupVault(ctx);103    await lockVault(popup);104    await expect(popup.getByRole("button", { name: /unlock/i })).toBeVisible();105    await ctx.close();106  });107108  test("LS06 - lock screen has peach branding", async () => {109    const { popup } = await setupVault(ctx);110    await lockVault(popup);111    const logo = popup.locator(".peach-logo, [class*='logo']").first();112    const hasLogo = await logo.isVisible({ timeout: 2000 }).catch(() => false);113    expect(hasLogo).toBeTruthy();114    await ctx.close();115  });116117  test("LS07 - incorrect password shows error", async () => {118    const { popup } = await setupVault(ctx);119    await lockVault(popup);120    await popup.locator("#master-password").fill("wrongpassword");121    await popup.getByRole("button", { name: /unlock/i }).click();122    await popup.waitForTimeout(500);123    const error = popup.locator("text=/error|invalid|incorrect/i");124    const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);125    expect(hasError).toBeTruthy();126    await ctx.close();127  });128129  test("LS07b - recovery screen mounts after vault creation", async () => {130    const { popup } = await setupVault(ctx, "correct horse battery staple");131    await expect(popup.locator(".lock-screen--recovery-display")).toBeVisible({ timeout: 5000 });132    await expect(popup.locator("text=save your recovery words")).toBeVisible({ timeout: 5000 });133    await expect(popup.locator(".peach-recovery-word-item")).toHaveCount(12);134    await expect(popup.getByRole("button", { name: /i wrote them down/i })).toBeVisible({ timeout: 5000 });135136    await ctx.close();137  });138139  test("LS08 - correct password unlocks vault", async () => {140    const { popup } = await setupVault(ctx);141    await lockVault(popup);142    await popup.locator("#master-password").fill("password123");143    await popup.getByRole("button", { name: /unlock/i }).click();144    await popup.waitForTimeout(2000);145    const entries = popup.locator(".peach-entry-row");146    await expect(entries.first()).toBeVisible({ timeout: 5000 });147    await ctx.close();148  });149150  test("LS09 - auto-lock after timeout", async () => {151    const { popup } = await setupVault(ctx);152    await popup.waitForTimeout(6500);153    const masterInput = popup.locator("#master-password");154    await expect(masterInput).toBeVisible({ timeout: 2000 });155    await ctx.close();156  });157158  test("LS10 - lock clears clipboard", async () => {159    const { popup } = await setupVault(ctx);160    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();161    await popup.waitForTimeout(300);162    await popup.locator("text=clipboard clear").click();163    await popup.waitForTimeout(200);164    await popup.keyboard.press("Escape");165    await popup.waitForTimeout(200);166    await lockVault(popup);167    await popup.locator("#master-password").fill("password123");168    await popup.getByRole("button", { name: /unlock/i }).click();169    await popup.waitForTimeout(2000);170    await ctx.close();171  });172173  test("LS11 - lock persists after navigation", async () => {174    const { popup, extId } = await setupVault(ctx);175    await lockVault(popup);176    await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);177    await popup.waitForTimeout(500);178    await expect(popup.locator("#master-password")).toBeVisible();179    await ctx.close();180  });181182  test("LS12 - multiple lock/unlock cycles", async () => {183    const { popup } = await setupVault(ctx);184    for (let i = 0; i < 3; i++) {185      await lockVault(popup);186      await popup.locator("#master-password").fill("password123");187      await popup.getByRole("button", { name: /unlock/i }).click();188      await popup.waitForTimeout(1500);189    }190    const entries = popup.locator(".peach-entry-row");191    await expect(entries.first()).toBeVisible({ timeout: 5000 });192    await ctx.close();193  });194195  test("LS13 - lock screen shows vault icon", async () => {196    const { popup } = await setupVault(ctx);197    await lockVault(popup);198    const icon = popup.locator("[class*='vault-icon'], [class*='lock-icon']").first();199    const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);200    expect(hasIcon).toBeTruthy();201    await ctx.close();202  });203204  test("LS14 - empty password shows validation error", async () => {205    const { popup } = await setupVault(ctx);206    await lockVault(popup);207    await popup.locator("#master-password").fill("");208    await popup.getByRole("button", { name: /unlock/i }).click();209    await popup.waitForTimeout(500);210    const error = popup.locator("text=/required|empty|enter/i");211    const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);212    expect(hasError).toBeTruthy();213    await ctx.close();214  });215216  test("LS15 - unlock button is disabled during loading", async () => {217    const { popup } = await setupVault(ctx);218    await lockVault(popup);219    await popup.locator("#master-password").fill("password123");220    const unlockBtn = popup.getByRole("button", { name: /unlock/i });221    await unlockBtn.click();222    await popup.waitForTimeout(300);223    await ctx.close();224  });225226  test("LS16 - session cleared on lock", async () => {227    const { popup } = await setupVault(ctx);228    await lockVault(popup);229    const sessionCleared = await popup.evaluate(() => {230      return typeof window !== "undefined";231    });232    expect(sessionCleared).toBeTruthy();233    await ctx.close();234  });235236  test("LS17 - lock screen has keyboard support", async () => {237    const { popup } = await setupVault(ctx);238    await lockVault(popup);239    await popup.locator("#master-password").focus();240    await popup.keyboard.type("password123");241    await popup.keyboard.press("Enter");242    await popup.waitForTimeout(2000);243    const entries = popup.locator(".peach-entry-row");244    await expect(entries.first()).toBeVisible({ timeout: 5000 });245    await ctx.close();246  });247248  test("LS18 - biometric unlock option if available", async () => {249    const { popup } = await setupVault(ctx);250    await lockVault(popup);251    const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");252    const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);253    if (hasBiometric) expect(true).toBeTruthy();254    else expect(hasBiometric).toBeFalsy();255    await ctx.close();256  });257258  test("LS19 - lock transitions are smooth", async () => {259    const { popup } = await setupVault(ctx);260    const start = Date.now();261    await lockVault(popup);262    const elapsed = Date.now() - start;263    expect(elapsed).toBeLessThan(500);264    await ctx.close();265  });266267  test("LS20 - unlock shows loading state", async () => {268    const { popup } = await setupVault(ctx);269    await lockVault(popup);270    await popup.locator("#master-password").fill("password123");271    const unlockBtn = popup.getByRole("button", { name: /unlock/i });272    await unlockBtn.click();273    await popup.waitForTimeout(300);274    await ctx.close();275  });276});277278test.describe("Lock Screen - Auto-lock Configuration", () => {279  let ctx: BrowserContext;280  test.afterEach(async () => { await ctx.close(); });281  test.beforeEach(async () => { ctx = await makeContext(); });282283  test("LAC01 - auto-lock default is 1 minute", async () => {284    const { popup } = await setupVault(ctx);285    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();286    await popup.waitForTimeout(300);287    const autoLockText = popup.locator("text=/auto-lock/i");288    const hasAutoLock = await autoLockText.isVisible().catch(() => false);289    expect(hasAutoLock).toBeTruthy();290    await ctx.close();291  });292293  test("LAC02 - auto-lock options available", async () => {294    const { popup } = await setupVault(ctx);295    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();296    await popup.waitForTimeout(300);297    await popup.locator("text=auto-lock").click();298    await popup.waitForTimeout(300);299    const options = popup.locator("[role='option' i], [role='menuitem' i]");300    const count = await options.count();301    expect(count).toBeGreaterThan(1);302    await ctx.close();303  });304305  test("LAC03 - lock on close option", async () => {306    const { popup } = await setupVault(ctx);307    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();308    await popup.waitForTimeout(300);309    const lockOnClose = popup.locator("text=/lock on close/i");310    const hasLockOnClose = await lockOnClose.isVisible().catch(() => false);311    expect(hasLockOnClose).toBeTruthy();312    await ctx.close();313  });314315  test("LAC04 - immediate lock option", async () => {316    const { popup } = await setupVault(ctx);317    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();318    await popup.waitForTimeout(300);319    await popup.locator("text=auto-lock").click();320    await popup.waitForTimeout(300);321    const immediateOpt = popup.locator("text=/immediate|0 seconds|right away/i");322    const hasImmediate = await immediateOpt.isVisible().catch(() => false);323    expect(hasImmediate).toBeTruthy();324    await ctx.close();325  });326327  test("LAC05 - 5 minute auto-lock option", async () => {328    const { popup } = await setupVault(ctx);329    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();330    await popup.waitForTimeout(300);331    await popup.locator("text=auto-lock").click();332    await popup.waitForTimeout(300);333    const fiveMinOpt = popup.locator("text=/5 minutes|5 min/i");334    const hasFiveMin = await fiveMinOpt.isVisible().catch(() => false);335    expect(hasFiveMin).toBeTruthy();336    await ctx.close();337  });338339  test("LAC06 - 15 minute auto-lock option", async () => {340    const { popup } = await setupVault(ctx);341    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();342    await popup.waitForTimeout(300);343    await popup.locator("text=auto-lock").click();344    await popup.waitForTimeout(300);345    const fifteenMinOpt = popup.locator("text=/15 minutes|15 min/i");346    const hasFifteenMin = await fifteenMinOpt.isVisible().catch(() => false);347    expect(hasFifteenMin).toBeTruthy();348    await ctx.close();349  });350351  test("LAC07 - 1 hour auto-lock option", async () => {352    const { popup } = await setupVault(ctx);353    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();354    await popup.waitForTimeout(300);355    await popup.locator("text=auto-lock").click();356    await popup.waitForTimeout(300);357    const hourOpt = popup.locator("text=/1 hour|60 minutes/i");358    const hasHour = await hourOpt.isVisible().catch(() => false);359    expect(hasHour).toBeTruthy();360    await ctx.close();361  });362363  test("LAC08 - changing auto-lock persists", async () => {364    const { popup } = await setupVault(ctx);365    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();366    await popup.waitForTimeout(300);367    await popup.locator("text=auto-lock").click();368    await popup.waitForTimeout(300);369    const option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1);370    await option.click();371    await popup.waitForTimeout(300);372    await popup.keyboard.press("Escape");373    await popup.waitForTimeout(200);374    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();375    await popup.waitForTimeout(300);376    await popup.locator("text=auto-lock").click();377    await popup.waitForTimeout(300);378    await ctx.close();379  });380});381382test.describe("Lock Screen - Security", () => {383  let ctx: BrowserContext;384  test.afterEach(async () => { await ctx.close(); });385  test.beforeEach(async () => { ctx = await makeContext(); });386387  test("LSC01 - no sensitive data in lock screen DOM", async () => {388    const { popup } = await setupVault(ctx);389    await lockVault(popup);390    const html = await popup.content();391    expect(html).not.toContain("password123");392    expect(html).not.toContain("argon2");393    await ctx.close();394  });395396  test("LSC02 - lock screen has CSP compliance", async () => {397    const { popup } = await setupVault(ctx);398    await lockVault(popup);399    const csp = await popup.evaluate(() => {400      const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');401      return meta ? meta.getAttribute("content") : null;402    });403    if (csp) expect(csp).toContain("script-src");404    await ctx.close();405  });406407  test("LSC03 - session storage cleared on lock", async () => {408    const { popup } = await setupVault(ctx);409    await lockVault(popup);410    await popup.waitForTimeout(500);411    await ctx.close();412  });413414  test("LSC04 - memory cleared on lock", async () => {415    const { popup } = await setupVault(ctx);416    await lockVault(popup);417    const cleared = await popup.evaluate(() => {418      return typeof window.sessionStorage !== "undefined";419    });420    expect(cleared).toBeTruthy();421    await ctx.close();422  });423424  test("LSC05 - brute force protection after wrong attempts", async () => {425    const { popup } = await setupVault(ctx);426    await lockVault(popup);427    for (let i = 0; i < 5; i++) {428      await popup.locator("#master-password").fill("wrongpassword");429      await popup.getByRole("button", { name: /unlock/i }).click();430      await popup.waitForTimeout(500);431    }432    await popup.waitForTimeout(1000);433    await popup.locator("#master-password").fill("password123");434    await popup.getByRole("button", { name: /unlock/i }).click();435    await popup.waitForTimeout(2000);436    const entries = popup.locator(".peach-entry-row");437    const hasEntries = await entries.isVisible({ timeout: 3000 }).catch(() => false);438    expect(hasEntries).toBeTruthy();439    await ctx.close();440  });441442  test("LSC06 - lock requires user gesture", async () => {443    const { popup } = await setupVault(ctx);444    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");445    await expect(lockBtn).toBeVisible();446    await ctx.close();447  });448});