import { test, expect, BrowserContext } from "@playwright/test"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import os from "os"; import fs from "fs/promises"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const EXTENSION_PATH = join(__dirname, "..", "dist"); async function makeContext() { const { chromium } = await import("playwright"); const userDataDir = join(os.tmpdir(), `peach-sec-${Date.now()}`); await fs.mkdir(userDataDir, { recursive: true }); return chromium.launchPersistentContext(userDataDir, { headless: false, args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`], }); } async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise { const start = Date.now(); let sw = ctx.serviceWorkers()[0]; while (!sw && Date.now() - start < maxWait) { await new Promise(r => setTimeout(r, 500)); sw = ctx.serviceWorkers()[0]; } if (!sw) throw new Error(`No SW after ${maxWait}ms`); return new URL(sw.url()).host; } async function setupVault(ctx: BrowserContext): Promise<{ popup: any, extId: string }> { const extId = await waitForSW(ctx); const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(3000); const pw = popup.locator("#master-password"); if (await pw.isVisible()) { await pw.fill("password123"); await popup.locator("#confirm-password").fill("password123"); await popup.getByRole("button", { name: /create vault/i }).click(); await popup.waitForTimeout(2000); } return { popup, extId }; } test.describe("Security Tests", () => { test("SEC02 - crypto.random used instead of Math.random", async () => { const distFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")); const jsFiles = distFiles.filter(f => f.endsWith(".js")); for (const file of jsFiles) { const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8"); const mathRandomMatches = content.match(/Math\.random\(\)/g) || []; const cryptoRandomMatches = content.match(/crypto\.getRandomValues\(/g) || []; if (mathRandomMatches.length > 0) { expect(cryptoRandomMatches.length).toBeGreaterThanOrEqual(mathRandomMatches.length); } } }); test("SEC03 - service worker sender validation", async () => { const { popup } = await setupVault(await makeContext()); const response = await popup.evaluate(async () => { return new Promise(resolve => { chrome.runtime.sendMessage({ type: "IS_UNLOCKED" }, r => resolve(r)); }); }); expect(response).toBeDefined(); }); test("SEC04 - manifest has correct permissions", async () => { const manifest = JSON.parse(await fs.readFile(join(__dirname, "..", "dist", "manifest.json"), "utf-8")); const allowed = ["storage", "activeTab", "scripting", "clipboardWrite", "alarms", "idle"]; const extra = manifest.permissions.filter((p: string) => !allowed.includes(p)); expect(extra).toHaveLength(0); }); test("SEC05 - no console.log in crypto paths", async () => { const cryptoFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")); const jsFiles = cryptoFiles.filter(f => f.endsWith(".js")); for (const file of jsFiles) { const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8"); const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const l = lines[i]; if (l.includes("console.log") && (l.includes("crypto") || l.includes("encrypt") || l.includes("argon") || l.includes("pbkdf"))) { expect(l).not.toMatch(/console\.log/); } } } }); test("SEC06 - TOTP uses SHA-256 or SHA-512", async () => { const totpFile = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f => f.find(f => f.includes("totp")) ); expect(totpFile).toBeDefined(); const content = await fs.readFile(join(__dirname, "..", "dist", "assets", totpFile!), "utf-8"); expect(content).toMatch(/SHA-256|SHA-512|sha256|sha512|createHash/); }); test("SEC07 - clipboard operations require explicit user action", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill("SecureTest"); await popup.locator('input[placeholder="domain.com"]').fill("https://secure.com"); await popup.locator('input[placeholder="username or email"]').fill("secure@test.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("securepass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=SecureTest").first().click(); await popup.waitForTimeout(500); const copyBtn = popup.locator(".peach-copy-btn").first(); let clicked = false; try { await copyBtn.click({ timeout: 2000 }); clicked = true; } catch {} expect(clicked).toBe(true); await ctx.close(); }); test("SEC08 - export requires password", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); await popup.getByRole("button", { name: /export/i }).click(); await popup.waitForTimeout(500); const exportPw = popup.locator('input[placeholder*="password" i]'); await expect(exportPw).toBeVisible(); await ctx.close(); }); test("SEC09 - vault locks on idle timeout", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(300); const autoLockRow = popup.locator("text=auto-lock").first(); await expect(autoLockRow).toBeVisible(); await ctx.close(); }); test("SEC10 - no sensitive data in error messages", async () => { const ctx = await makeContext(); await setupVault(ctx); const newPopup = await ctx.newPage(); const extId = (await waitForSW(ctx)); await newPopup.goto(`chrome-extension://${extId}/src/popup/index.html`); await newPopup.waitForTimeout(2000); await newPopup.locator("#master-password").fill("wrongpassword"); await newPopup.getByRole("button", { name: /unlock/i }).click(); await newPopup.waitForTimeout(500); const hint = newPopup.locator(".peach-hint-error, .peach-hint").first(); const hintText = await hint.textContent().catch(() => "") ?? ""; expect(hintText.toLowerCase()).not.toMatch(/password|secret|key|master/); await ctx.close(); }); }); test.describe("CSS Visual Compliance", () => { test("CSS01 - only animatable properties used in animations", async () => { const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f => f.filter(f => f.endsWith(".css")) ); const animatableProps = ["opacity", "transform", "color", "stroke", "stroke-dashoffset", "width", "max-height", "height"]; for (const file of cssFiles) { const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8"); const keyframes = content.match(/@keyframes[\s\S]*?\{[\s\S]*?\}/g) || []; for (const kf of keyframes) { const props = kf.match(/[\w-]+:\s*[^;]+;/g) || []; for (const prop of props) { const name = prop.split(":")[0].trim(); if (!animatableProps.includes(name)) { expect(name).toBe(""); } } } } }); test("CSS02 - only --spring, --retreat, --settle easing variables", async () => { const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f => f.filter(f => f.endsWith(".css")) ); for (const file of cssFiles) { const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8"); const cubicBezierMatches = content.match(/cubic-bezier\([^)]+\)/g) || []; for (const cb of cubicBezierMatches) { expect(content).toMatch(new RegExp(`--spring|--retreat|--settle|.${cb}.|`)); } } }); test("CSS03 - no Google Fonts CDN links", async () => { const html = await fs.readFile(join(__dirname, "..", "dist", "src", "popup", "index.html"), "utf-8"); expect(html).not.toMatch(/fonts\.googleapis\.com|fonts\.gstatic\.com/); }); test("CSS04 - self-hosted woff2 fonts only", async () => { const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f => f.filter(f => f.endsWith(".css")) ); for (const file of cssFiles) { const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8"); expect(content).not.toMatch(/url\(["']?https?:\/\//); } }); test("CSS05 - Argon2id params in crypto code", async () => { const jsFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f => f.filter(f => f.endsWith(".js")) ); for (const file of jsFiles) { const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8"); if (content.includes("argon") || content.includes("Argon2")) { expect(content).toMatch(/m=65536|t=3|p=4|iterations|memory/); break; } } }); test("CSS06 - scrollbar styling minimal and subtle", async () => { const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f => f.filter(f => f.endsWith(".css")) ); for (const file of cssFiles) { const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8"); const scrollbarWidth = content.match(/::-webkit-scrollbar\s*\{[\s\S]*?width:\s*(\d+)px/); if (scrollbarWidth) { expect(parseInt(scrollbarWidth[1])).toBeLessThanOrEqual(4); } } }); test("CSS07 - popup dimensions 320px width fixed", async () => { const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f => f.filter(f => f.endsWith(".css")) ); for (const file of cssFiles) { const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8"); const widthMatch = content.match(/width:\s*(\d+)px/); if (widthMatch && parseInt(widthMatch[1]) > 320) { expect(widthMatch[1]).toBe("320"); } } }); test("CSS08 - no overflow on body", async () => { const cssFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f => f.filter(f => f.endsWith(".css")) ); for (const file of cssFiles) { const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8"); const bodyOverflow = content.match(/body\s*\{[\s\S]*?overflow:\s*([^;]+)/); if (bodyOverflow) { expect(bodyOverflow[1].trim()).toBe("hidden"); } } }); }); test.describe("Performance & Responsiveness", () => { test("PERF01 - vault unlocks within 3 seconds", async () => { const ctx = await makeContext(); await setupVault(ctx); const newPopup = await ctx.newPage(); const extId = await waitForSW(ctx); await newPopup.goto(`chrome-extension://${extId}/src/popup/index.html`); await newPopup.waitForTimeout(1500); const start = Date.now(); await newPopup.locator("#master-password").fill("password123"); await newPopup.getByRole("button", { name: /unlock/i }).click(); await newPopup.waitForTimeout(500); const elapsed = Date.now() - start; expect(elapsed).toBeLessThan(5000); await ctx.close(); }); test("PERF02 - entry list renders smoothly", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); for (const name of ["A", "B", "C", "D", "E", "F", "G", "H"]) { await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill(name + "Test"); await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`); await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@test.com`); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(500); } await popup.waitForTimeout(1000); const entries = popup.locator("[class*=entry]"); const count = await entries.count(); expect(count).toBeGreaterThanOrEqual(7); await ctx.close(); }); test("PERF03 - search filters entries without delay", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); for (const name of ["Alpha", "Beta", "Gamma"]) { await popup.locator('input[placeholder="search vault..."]').click(); await popup.getByRole("button", { name: /new entry/i }).click(); await popup.locator(".peach-new-name-input").fill(name); await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`); await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@test.com`); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(500); } const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first(); await searchInput.click(); await searchInput.fill("Alpha"); await popup.waitForTimeout(300); const filtered = popup.locator("text=Alpha"); await expect(filtered).toBeVisible(); await ctx.close(); }); test("PERF04 - settings panel opens instantly", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); const start = Date.now(); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(200); const elapsed = Date.now() - start; expect(elapsed).toBeLessThan(1000); await ctx.close(); }); });