import { test, expect, type Page, type BrowserContext } from "@playwright/test"; import { fileURLToPath } from "node:url"; const PEACH_FILE = fileURLToPath(new URL("./fixtures/synthetic-backup.peach", import.meta.url)); import { UNLOCKED, VAULT_PASSWORD, completeRecoveryConfirmation, loadPopup, lockVault, makeContext, openSettings, setupVault, unlockVault, } from "./helpers/vault"; // Every test creates a real vault (onboarding + Argon2id KDF + recovery // confirmation ≈ 12s), and lock/unlock cycles run Argon2 again. The global // 30s cap is too tight for the multi-cycle cases; raise it for this file. test.describe.configure({ timeout: 90_000 }); // The auto-lock control is a native inside the (initially // concealed) Protection card. Option/value assertions read the DOM directly, // which works regardless of the card-deck animation; LAC08 uses a forced // selectOption to set the value without driving the deck. test("LAC01 - auto-lock defaults to 5 minutes", async () => { const { popup } = await setupVault(ctx); await openSettings(popup); await expect(autoLockSelect(popup)).toHaveValue("5"); await ctx.close(); }); test("LAC02 - auto-lock options available", async () => { const { popup } = await setupVault(ctx); await openSettings(popup); await expect(autoLockSelect(popup).locator("option")).toHaveCount(5); await ctx.close(); }); test("LAC03 - lock on close option present", async () => { const { popup } = await setupVault(ctx); await openSettings(popup); await expect( popup.locator(".peach-settings-row", { hasText: "Lock on close" }), ).toHaveCount(1); await ctx.close(); }); // Assert by option value + exact text. hasText is a substring match, so // "5 minutes" would also match "1<5 minutes>" — value scoping avoids that. test("LAC04 - never auto-lock option available", async () => { const { popup } = await setupVault(ctx); await openSettings(popup); await expect(autoLockSelect(popup).locator('option[value="0"]')).toHaveText("Never"); await ctx.close(); }); test("LAC05 - 5 minute auto-lock option", async () => { const { popup } = await setupVault(ctx); await openSettings(popup); await expect(autoLockSelect(popup).locator('option[value="5"]')).toHaveText("5 minutes"); await ctx.close(); }); test("LAC06 - 15 minute auto-lock option", async () => { const { popup } = await setupVault(ctx); await openSettings(popup); await expect(autoLockSelect(popup).locator('option[value="15"]')).toHaveText("15 minutes"); await ctx.close(); }); test("LAC07 - 30 minute auto-lock option", async () => { // The control maxes at 30 minutes — there is no "1 hour" option. const { popup } = await setupVault(ctx); await openSettings(popup); await expect(autoLockSelect(popup).locator('option[value="30"]')).toHaveText("30 minutes"); await ctx.close(); }); test("LAC08 - changing auto-lock persists across reload", async () => { const { popup, extId } = await setupVault(ctx); await openSettings(popup); // force: bypass the card-deck animation; selectOption still fires onChange. await autoLockSelect(popup).selectOption("15", { force: true }); await expect(autoLockSelect(popup)).toHaveValue("15"); // The setting is read back from storage on mount, so a reload proves it stuck. await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(1500); if (await popup.locator("#unlock-password").isVisible().catch(() => false)) { await unlockVault(popup); } await openSettings(popup); await expect(autoLockSelect(popup)).toHaveValue("15"); await ctx.close(); }); }); test.describe("Lock Screen - Security", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("LSC01 - no sensitive data in lock screen DOM", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const html = await popup.content(); expect(html).not.toContain(VAULT_PASSWORD); expect(html).not.toContain("argon2"); await ctx.close(); }); test("LSC02 - lock screen has CSP compliance", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const csp = await popup.evaluate(() => { const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]'); return meta ? meta.getAttribute("content") : null; }); if (csp) expect(csp).toContain("script-src"); await ctx.close(); }); test("LSC03 - session storage cleared on lock", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); await popup.waitForTimeout(500); await ctx.close(); }); test("LSC04 - memory cleared on lock", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); const cleared = await popup.evaluate(() => { return typeof window.sessionStorage !== "undefined"; }); expect(cleared).toBeTruthy(); await ctx.close(); }); test("LSC05 - correct password still unlocks after wrong attempts", async () => { const { popup } = await setupVault(ctx); await lockVault(popup); for (let i = 0; i < 3; i++) { await popup.locator("#unlock-password").fill("wrongpassword"); await popup.getByRole("button", { name: /unlock/i }).click(); // A wrong password keeps us on the lock screen (each unlock runs Argon2, // so wait for the input to settle before the next attempt). await expect(popup.locator("#unlock-password")).toBeVisible({ timeout: 15000 }); await popup.waitForTimeout(500); } // The correct password must still work — wrong attempts don't lock you out. await unlockVault(popup); await ctx.close(); }); test("LSC06 - lock requires user gesture", async () => { const { popup } = await setupVault(ctx); const lockBtn = popup.locator(UNLOCKED); await expect(lockBtn).toBeVisible(); await ctx.close(); }); });