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-kbd-${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("Keyboard Navigation", () => { test("KB01 - enter submits password on lock screen", async () => { const ctx = await makeContext(); const { popup } = 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("password123"); await newPopup.keyboard.press("Enter"); await newPopup.waitForTimeout(2000); const entries = newPopup.locator("[class*=entry]"); await expect(entries.first()).toBeVisible({ timeout: 5000 }); await ctx.close(); }); test("KB02 - tab navigates through fields", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(500); await popup.keyboard.press("Tab"); await popup.keyboard.press("Tab"); const focused = await popup.evaluate(() => document.activeElement?.tagName); expect(focused).toBeTruthy(); await ctx.close(); }); test("KB03 - escape closes settings panel", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(500); await popup.keyboard.press("Escape"); await popup.waitForTimeout(300); const overlay = popup.locator(".peach-overlay-view"); const isHidden = await overlay.isHidden().catch(() => true); expect(isHidden).toBeTruthy(); await ctx.close(); }); test("KB04 - arrow keys navigate entry list", 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); } await popup.locator('input[placeholder="search vault..."]').click(); await popup.keyboard.press("ArrowDown"); await popup.waitForTimeout(200); await ctx.close(); }); test("KB05 - enter opens entry from list", 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("EnterTest"); await popup.locator('input[placeholder="domain.com"]').fill("https://entertest.com"); await popup.locator('input[placeholder="username or email"]').fill("enter@test.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1000); await popup.locator('input[placeholder="search vault..."]').click(); await popup.waitForTimeout(300); await popup.keyboard.press("Enter"); await popup.waitForTimeout(500); await ctx.close(); }); test("KB06 - ctrl+k focuses search", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.keyboard.press("Control+k"); await popup.waitForTimeout(200); const searchFocused = await popup.evaluate(() => { const el = document.activeElement; return el?.classList.contains("peach-search-input") || el?.closest(".peach-search-input"); }); expect(searchFocused).toBeTruthy(); await ctx.close(); }); test("KB07 - backspace clears search", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator('input[placeholder="search vault..."]').click(); await popup.locator('input[placeholder="search vault..."]').fill("test"); await popup.keyboard.press("Backspace"); await popup.keyboard.press("Backspace"); await popup.keyboard.press("Backspace"); await popup.keyboard.press("Backspace"); await ctx.close(); }); }); test.describe("Edge Cases & Error Handling", () => { test("EDGE01 - entry with very long title", 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(); const longTitle = "A".repeat(200); await popup.locator(".peach-new-name-input").fill(longTitle); await popup.locator('input[placeholder="domain.com"]').fill("https://longtitle.com"); await popup.locator('input[placeholder="username or email"]').fill("user@test.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await ctx.close(); }); test("EDGE02 - entry with unicode in all fields", 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("日本語テスト"); await popup.locator('input[placeholder="domain.com"]').fill("https://日本語.com"); await popup.locator('input[placeholder="username or email"]').fill("用户@例子.测试"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("密码🔐"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await expect(popup.locator("text=日本語テスト")).toBeVisible({ timeout: 3000 }); await ctx.close(); }); test("EDGE03 - very short password accepted", 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("ShortPass"); await popup.locator('input[placeholder="domain.com"]').fill("https://short.com"); await popup.locator('input[placeholder="username or email"]').fill("user@short.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("a"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await ctx.close(); }); test("EDGE04 - entry with special chars in domain", 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("SpecialDomain"); await popup.locator('input[placeholder="domain.com"]').fill("https://sub.domain.with-dashes.and_underscores.example.com/path?query=value"); await popup.locator('input[placeholder="username or email"]').fill("user@special.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("specialpass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await ctx.close(); }); test("EDGE05 - entry with no username", 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("NoUsername"); await popup.locator('input[placeholder="domain.com"]').fill("https://nouser.com"); await popup.locator('input[placeholder="username or email"]').fill(""); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await ctx.close(); }); test("EDGE06 - entry with no domain", 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("NoDomain"); await popup.locator('input[placeholder="domain.com"]').fill(""); await popup.locator('input[placeholder="username or email"]').fill("user@test.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await ctx.close(); }); test("EDGE07 - rapid add entry button clicks", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); const addBtn = popup.getByRole("button", { name: /new entry/i }); await addBtn.click(); await addBtn.click(); await addBtn.click(); await popup.waitForTimeout(500); const modal = popup.locator(".peach-new-body"); await expect(modal).toBeVisible({ timeout: 3000 }); await ctx.close(); }); test("EDGE08 - search with special regex chars", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); for (const name of ["Test[0]", "Test(1)", "Test{2}"]) { 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().replace(/[^a-z]/g, "")}.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("[0]"); await popup.waitForTimeout(300); await ctx.close(); }); test("EDGE09 - copy button during loading state", 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("CopyLoading"); await popup.locator('input[placeholder="domain.com"]').fill("https://copyloading.com"); await popup.locator('input[placeholder="username or email"]').fill("user@copy.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const copyBtn = popup.locator(".peach-copy-btn").first(); if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) { await copyBtn.click(); await popup.waitForTimeout(500); } await ctx.close(); }); test("EDGE10 - unlock with whitespace password", async () => { const ctx = await makeContext(); const extId = await waitForSW(ctx); const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(2000); await popup.locator("#master-password").fill(" "); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(500); const errorMsg = popup.locator(".peach-hint-error, .peach-hint"); const hasError = await errorMsg.first().isVisible({ timeout: 2000 }).catch(() => false); await ctx.close(); }); }); test.describe("Import Export Round-trip", () => { test("IMP01 - import CSV and verify entries", 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: /import/i }).click(); await popup.waitForTimeout(500); const fileInput = popup.locator('input[type="file"]'); const csvContent = "title,username,password,url\nTestImport,importuser,importpass,https://import.com"; await fileInput.setInputFiles({ name: "test.csv", mimeType: "text/csv", buffer: Buffer.from(csvContent) }); await popup.waitForTimeout(1000); await ctx.close(); }); test("IMP02 - export creates valid JSON file", 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("ExportTest"); await popup.locator('input[placeholder="domain.com"]').fill("https://exporttest.com"); await popup.locator('input[placeholder="username or email"]').fill("export@user.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("exportpass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); 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); await ctx.close(); }); test("IMP03 - empty vault export works", 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); await ctx.close(); }); test("IMP04 - import 1Password JSON format", 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: /import/i }).click(); await popup.waitForTimeout(500); const fileInput = popup.locator('input[type="file"]'); const json1pContent = JSON.stringify({ accounts: [{ vaults: [{ items: [{ overview: { title: "1P Test", url: "https://1p.com" }, details: { loginFields: [ { designation: "username", value: "testuser" }, { designation: "password", value: "testpass" } ] } }] }] }] }); await fileInput.setInputFiles({ name: "1p-export.json", mimeType: "application/json", buffer: Buffer.from(json1pContent) }); await popup.waitForTimeout(1000); await ctx.close(); }); }); test.describe("Password Generator", () => { test("GEN01 - generates passwords of exact length", 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: /password generator/i }).click(); await popup.waitForTimeout(500); const slider = popup.locator(".peach-pwgen-slider"); await slider.fill("32"); await popup.waitForTimeout(300); const output = await popup.locator(".peach-pwgen-output").textContent(); expect(output?.length).toBe(32); await ctx.close(); }); test("GEN02 - regenerate produces new 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: /password generator/i }).click(); await popup.waitForTimeout(500); const pwd1 = await popup.locator(".peach-pwgen-output").textContent(); await popup.locator("button[aria-label='Regenerate']").click(); await popup.waitForTimeout(300); const pwd2 = await popup.locator(".peach-pwgen-output").textContent(); expect(pwd1).not.toEqual(pwd2); await ctx.close(); }); test("GEN03 - toggle uppercase on/off", 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: /password generator/i }).click(); await popup.waitForTimeout(500); const toggles = popup.locator(".peach-pwgen-toggle-row"); const count = await toggles.count(); if (count >= 2) { await toggles.nth(0).click(); await popup.waitForTimeout(200); } await ctx.close(); }); test("GEN04 - toggle numbers on/off", 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: /password generator/i }).click(); await popup.waitForTimeout(500); const toggles = popup.locator(".peach-pwgen-toggle-row"); const count = await toggles.count(); if (count >= 3) { await toggles.nth(2).click(); await popup.waitForTimeout(200); } await ctx.close(); }); test("GEN05 - toggle symbols on/off", 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: /password generator/i }).click(); await popup.waitForTimeout(500); const toggles = popup.locator(".peach-pwgen-toggle-row"); const count = await toggles.count(); if (count >= 4) { await toggles.nth(3).click(); await popup.waitForTimeout(200); } await ctx.close(); }); test("GEN06 - min length 8 enforced", 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: /password generator/i }).click(); await popup.waitForTimeout(500); const slider = popup.locator(".peach-pwgen-slider"); await slider.fill("4"); await popup.waitForTimeout(300); const output = await popup.locator(".peach-pwgen-output").textContent(); expect(output?.length).toBeGreaterThanOrEqual(8); await ctx.close(); }); test("GEN07 - max length 64 enforced", 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: /password generator/i }).click(); await popup.waitForTimeout(500); const slider = popup.locator(".peach-pwgen-slider"); await slider.fill("128"); await popup.waitForTimeout(300); const output = await popup.locator(".peach-pwgen-output").textContent(); expect(output?.length).toBeLessThanOrEqual(64); await ctx.close(); }); test("GEN08 - use password fills into entry", 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: /password generator/i }).click(); await popup.waitForTimeout(500); const useBtn = popup.getByRole("button", { name: /use/i }); if (await useBtn.isVisible({ timeout: 2000 }).catch(() => false)) { await useBtn.click(); await popup.waitForTimeout(500); } await ctx.close(); }); });