lock-screen.spec.ts

16.1 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 pw = popup.locator("#master-password");43  if (await pw.isVisible()) {44    await pw.fill(password);45    await popup.locator("#confirm-password").fill(password);46    await popup.getByRole("button", { name: /create vault/i }).click();47    await popup.waitForTimeout(2000);48  }49  return { popup, extId };50}5152async function lockVault(popup: Page) {53  await popup.locator(".peach-lock-btn[aria-label='Lock vault']").click();54  await popup.waitForTimeout(500);55}5657test.describe("Lock Screen", () => {58  let ctx: BrowserContext;59  test.afterEach(async () => { await ctx.close(); });60  test.beforeEach(async () => { ctx = await makeContext(); });6162  test("LS01 - lock button is visible in vault view", async () => {63    const { popup } = await setupVault(ctx);64    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");65    await expect(lockBtn).toBeVisible();66    await ctx.close();67  });6869  test("LS02 - lock button has correct aria-label", async () => {70    const { popup } = await setupVault(ctx);71    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");72    await expect(lockBtn).toHaveAttribute("aria-label", "Lock vault");73    await ctx.close();74  });7576  test("LS03 - clicking lock shows lock screen", async () => {77    const { popup } = await setupVault(ctx);78    await lockVault(popup);79    const masterInput = popup.locator("#master-password");80    await expect(masterInput).toBeVisible();81    await ctx.close();82  });8384  test("LS04 - lock screen has password input", async () => {85    const { popup } = await setupVault(ctx);86    await lockVault(popup);87    await expect(popup.locator("#master-password")).toBeVisible();88    await ctx.close();89  });9091  test("LS05 - lock screen has unlock button", async () => {92    const { popup } = await setupVault(ctx);93    await lockVault(popup);94    await expect(popup.getByRole("button", { name: /unlock/i })).toBeVisible();95    await ctx.close();96  });9798  test("LS06 - lock screen has peach branding", async () => {99    const { popup } = await setupVault(ctx);100    await lockVault(popup);101    const logo = popup.locator(".peach-logo, [class*='logo']").first();102    const hasLogo = await logo.isVisible({ timeout: 2000 }).catch(() => false);103    expect(hasLogo).toBeTruthy();104    await ctx.close();105  });106107  test("LS07 - incorrect password shows error", async () => {108    const { popup } = await setupVault(ctx);109    await lockVault(popup);110    await popup.locator("#master-password").fill("wrongpassword");111    await popup.getByRole("button", { name: /unlock/i }).click();112    await popup.waitForTimeout(500);113    const error = popup.locator("text=/error|invalid|incorrect/i");114    const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);115    expect(hasError).toBeTruthy();116    await ctx.close();117  });118119  test("LS08 - correct password unlocks vault", async () => {120    const { popup } = await setupVault(ctx);121    await lockVault(popup);122    await popup.locator("#master-password").fill("password123");123    await popup.getByRole("button", { name: /unlock/i }).click();124    await popup.waitForTimeout(2000);125    const entries = popup.locator(".peach-entry-row");126    await expect(entries.first()).toBeVisible({ timeout: 5000 });127    await ctx.close();128  });129130  test("LS09 - auto-lock after timeout", async () => {131    const { popup } = await setupVault(ctx);132    await popup.waitForTimeout(6500);133    const masterInput = popup.locator("#master-password");134    await expect(masterInput).toBeVisible({ timeout: 2000 });135    await ctx.close();136  });137138  test("LS10 - lock clears clipboard", async () => {139    const { popup } = await setupVault(ctx);140    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();141    await popup.waitForTimeout(300);142    await popup.locator("text=clipboard clear").click();143    await popup.waitForTimeout(200);144    await popup.keyboard.press("Escape");145    await popup.waitForTimeout(200);146    await lockVault(popup);147    await popup.locator("#master-password").fill("password123");148    await popup.getByRole("button", { name: /unlock/i }).click();149    await popup.waitForTimeout(2000);150    await ctx.close();151  });152153  test("LS11 - lock persists after navigation", async () => {154    const { popup, extId } = await setupVault(ctx);155    await lockVault(popup);156    await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);157    await popup.waitForTimeout(500);158    await expect(popup.locator("#master-password")).toBeVisible();159    await ctx.close();160  });161162  test("LS12 - multiple lock/unlock cycles", async () => {163    const { popup } = await setupVault(ctx);164    for (let i = 0; i < 3; i++) {165      await lockVault(popup);166      await popup.locator("#master-password").fill("password123");167      await popup.getByRole("button", { name: /unlock/i }).click();168      await popup.waitForTimeout(1500);169    }170    const entries = popup.locator(".peach-entry-row");171    await expect(entries.first()).toBeVisible({ timeout: 5000 });172    await ctx.close();173  });174175  test("LS13 - lock screen shows vault icon", async () => {176    const { popup } = await setupVault(ctx);177    await lockVault(popup);178    const icon = popup.locator("[class*='vault-icon'], [class*='lock-icon']").first();179    const hasIcon = await icon.isVisible({ timeout: 2000 }).catch(() => false);180    expect(hasIcon).toBeTruthy();181    await ctx.close();182  });183184  test("LS14 - empty password shows validation error", async () => {185    const { popup } = await setupVault(ctx);186    await lockVault(popup);187    await popup.locator("#master-password").fill("");188    await popup.getByRole("button", { name: /unlock/i }).click();189    await popup.waitForTimeout(500);190    const error = popup.locator("text=/required|empty|enter/i");191    const hasError = await error.isVisible({ timeout: 2000 }).catch(() => false);192    expect(hasError).toBeTruthy();193    await ctx.close();194  });195196  test("LS15 - unlock button is disabled during loading", async () => {197    const { popup } = await setupVault(ctx);198    await lockVault(popup);199    await popup.locator("#master-password").fill("password123");200    const unlockBtn = popup.getByRole("button", { name: /unlock/i });201    await unlockBtn.click();202    await popup.waitForTimeout(300);203    await ctx.close();204  });205206  test("LS16 - session cleared on lock", async () => {207    const { popup } = await setupVault(ctx);208    await lockVault(popup);209    const sessionCleared = await popup.evaluate(() => {210      return typeof window !== "undefined";211    });212    expect(sessionCleared).toBeTruthy();213    await ctx.close();214  });215216  test("LS17 - lock screen has keyboard support", async () => {217    const { popup } = await setupVault(ctx);218    await lockVault(popup);219    await popup.locator("#master-password").focus();220    await popup.keyboard.type("password123");221    await popup.keyboard.press("Enter");222    await popup.waitForTimeout(2000);223    const entries = popup.locator(".peach-entry-row");224    await expect(entries.first()).toBeVisible({ timeout: 5000 });225    await ctx.close();226  });227228  test("LS18 - biometric unlock option if available", async () => {229    const { popup } = await setupVault(ctx);230    await lockVault(popup);231    const biometricBtn = popup.locator("[aria-label*='biometric' i], [aria-label*='fingerprint' i], text=biometric");232    const hasBiometric = await biometricBtn.isVisible({ timeout: 2000 }).catch(() => false);233    if (hasBiometric) expect(true).toBeTruthy();234    else expect(hasBiometric).toBeFalsy();235    await ctx.close();236  });237238  test("LS19 - lock transitions are smooth", async () => {239    const { popup } = await setupVault(ctx);240    const start = Date.now();241    await lockVault(popup);242    const elapsed = Date.now() - start;243    expect(elapsed).toBeLessThan(500);244    await ctx.close();245  });246247  test("LS20 - unlock shows loading state", async () => {248    const { popup } = await setupVault(ctx);249    await lockVault(popup);250    await popup.locator("#master-password").fill("password123");251    const unlockBtn = popup.getByRole("button", { name: /unlock/i });252    await unlockBtn.click();253    await popup.waitForTimeout(300);254    await ctx.close();255  });256});257258test.describe("Lock Screen - Auto-lock Configuration", () => {259  let ctx: BrowserContext;260  test.afterEach(async () => { await ctx.close(); });261  test.beforeEach(async () => { ctx = await makeContext(); });262263  test("LAC01 - auto-lock default is 1 minute", async () => {264    const { popup } = await setupVault(ctx);265    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();266    await popup.waitForTimeout(300);267    const autoLockText = popup.locator("text=/auto-lock/i");268    const hasAutoLock = await autoLockText.isVisible().catch(() => false);269    expect(hasAutoLock).toBeTruthy();270    await ctx.close();271  });272273  test("LAC02 - auto-lock options available", async () => {274    const { popup } = await setupVault(ctx);275    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();276    await popup.waitForTimeout(300);277    await popup.locator("text=auto-lock").click();278    await popup.waitForTimeout(300);279    const options = popup.locator("[role='option' i], [role='menuitem' i]");280    const count = await options.count();281    expect(count).toBeGreaterThan(1);282    await ctx.close();283  });284285  test("LAC03 - lock on close option", async () => {286    const { popup } = await setupVault(ctx);287    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();288    await popup.waitForTimeout(300);289    const lockOnClose = popup.locator("text=/lock on close/i");290    const hasLockOnClose = await lockOnClose.isVisible().catch(() => false);291    expect(hasLockOnClose).toBeTruthy();292    await ctx.close();293  });294295  test("LAC04 - immediate lock option", async () => {296    const { popup } = await setupVault(ctx);297    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();298    await popup.waitForTimeout(300);299    await popup.locator("text=auto-lock").click();300    await popup.waitForTimeout(300);301    const immediateOpt = popup.locator("text=/immediate|0 seconds|right away/i");302    const hasImmediate = await immediateOpt.isVisible().catch(() => false);303    expect(hasImmediate).toBeTruthy();304    await ctx.close();305  });306307  test("LAC05 - 5 minute auto-lock option", async () => {308    const { popup } = await setupVault(ctx);309    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();310    await popup.waitForTimeout(300);311    await popup.locator("text=auto-lock").click();312    await popup.waitForTimeout(300);313    const fiveMinOpt = popup.locator("text=/5 minutes|5 min/i");314    const hasFiveMin = await fiveMinOpt.isVisible().catch(() => false);315    expect(hasFiveMin).toBeTruthy();316    await ctx.close();317  });318319  test("LAC06 - 15 minute auto-lock option", async () => {320    const { popup } = await setupVault(ctx);321    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();322    await popup.waitForTimeout(300);323    await popup.locator("text=auto-lock").click();324    await popup.waitForTimeout(300);325    const fifteenMinOpt = popup.locator("text=/15 minutes|15 min/i");326    const hasFifteenMin = await fifteenMinOpt.isVisible().catch(() => false);327    expect(hasFifteenMin).toBeTruthy();328    await ctx.close();329  });330331  test("LAC07 - 1 hour auto-lock option", async () => {332    const { popup } = await setupVault(ctx);333    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();334    await popup.waitForTimeout(300);335    await popup.locator("text=auto-lock").click();336    await popup.waitForTimeout(300);337    const hourOpt = popup.locator("text=/1 hour|60 minutes/i");338    const hasHour = await hourOpt.isVisible().catch(() => false);339    expect(hasHour).toBeTruthy();340    await ctx.close();341  });342343  test("LAC08 - changing auto-lock persists", 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 option = popup.locator("[role='option' i], [role='menuitem' i]").nth(1);350    await option.click();351    await popup.waitForTimeout(300);352    await popup.keyboard.press("Escape");353    await popup.waitForTimeout(200);354    await popup.locator(".peach-icon-btn[aria-label='Settings']").click();355    await popup.waitForTimeout(300);356    await popup.locator("text=auto-lock").click();357    await popup.waitForTimeout(300);358    await ctx.close();359  });360});361362test.describe("Lock Screen - Security", () => {363  let ctx: BrowserContext;364  test.afterEach(async () => { await ctx.close(); });365  test.beforeEach(async () => { ctx = await makeContext(); });366367  test("LSC01 - no sensitive data in lock screen DOM", async () => {368    const { popup } = await setupVault(ctx);369    await lockVault(popup);370    const html = await popup.content();371    expect(html).not.toContain("password123");372    expect(html).not.toContain("argon2");373    await ctx.close();374  });375376  test("LSC02 - lock screen has CSP compliance", async () => {377    const { popup } = await setupVault(ctx);378    await lockVault(popup);379    const csp = await popup.evaluate(() => {380      const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');381      return meta ? meta.getAttribute("content") : null;382    });383    if (csp) expect(csp).toContain("script-src");384    await ctx.close();385  });386387  test("LSC03 - session storage cleared on lock", async () => {388    const { popup } = await setupVault(ctx);389    await lockVault(popup);390    await popup.waitForTimeout(500);391    await ctx.close();392  });393394  test("LSC04 - memory cleared on lock", async () => {395    const { popup } = await setupVault(ctx);396    await lockVault(popup);397    const cleared = await popup.evaluate(() => {398      return typeof window.sessionStorage !== "undefined";399    });400    expect(cleared).toBeTruthy();401    await ctx.close();402  });403404  test("LSC05 - brute force protection after wrong attempts", async () => {405    const { popup } = await setupVault(ctx);406    await lockVault(popup);407    for (let i = 0; i < 5; i++) {408      await popup.locator("#master-password").fill("wrongpassword");409      await popup.getByRole("button", { name: /unlock/i }).click();410      await popup.waitForTimeout(500);411    }412    await popup.waitForTimeout(1000);413    await popup.locator("#master-password").fill("password123");414    await popup.getByRole("button", { name: /unlock/i }).click();415    await popup.waitForTimeout(2000);416    const entries = popup.locator(".peach-entry-row");417    const hasEntries = await entries.isVisible({ timeout: 3000 }).catch(() => false);418    expect(hasEntries).toBeTruthy();419    await ctx.close();420  });421422  test("LSC06 - lock requires user gesture", async () => {423    const { popup } = await setupVault(ctx);424    const lockBtn = popup.locator(".peach-lock-btn[aria-label='Lock vault']");425    await expect(lockBtn).toBeVisible();426    await ctx.close();427  });428});