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"); const VAULT_PASSWORD = "CorrectHorseBattery42!"; async function makeContext() { const { chromium } = await import("playwright"); const userDataDir = join(os.tmpdir(), `peach-entries-${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(VAULT_PASSWORD); await popup.locator("#confirm-password").fill(VAULT_PASSWORD); await popup.waitForFunction(() => { const submit = document.querySelector(".peach-lock-submit"); return submit instanceof HTMLButtonElement && !submit.disabled; }); await popup.getByRole("button", { name: /create vault/i }).click(); await popup.waitForTimeout(2000); } return { popup, extId }; } async function seedEntries(popup: any, names: string[]) { for (const name of names) { await popup.evaluate(async (entryName: string) => { await new Promise((resolve) => { chrome.runtime.sendMessage( { type: "ADD_ENTRY", payload: { type: "login", title: entryName, url: `https://${entryName.toLowerCase()}.com`, username: `${entryName.toLowerCase()}@test.com`, password: "Passphrase123!", }, }, resolve ); }); }, name); } } test.describe("Entry Interaction Flows", () => { test("FLOW01 - full add entry workflow", 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("GitHub"); await popup.locator('input[placeholder="domain.com"]').fill("https://github.com"); await popup.locator('input[placeholder="username or email"]').fill("june@asha.software"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("supersecret123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await expect(popup.locator("text=GitHub")).toBeVisible({ timeout: 3000 }); await ctx.close(); }); test("FLOW02 - edit entry workflow", 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("EditFlow"); await popup.locator('input[placeholder="domain.com"]').fill("https://editflow.com"); await popup.locator('input[placeholder="username or email"]').fill("edit@flow.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("oldpass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=EditFlow").first().click(); await popup.waitForTimeout(500); const editBtn = popup.locator("button[aria-label='Edit']").first(); await editBtn.click(); await popup.waitForTimeout(500); const newPwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await newPwdField.fill("newpass"); await popup.getByRole("button", { name: /save/i }).click(); await popup.waitForTimeout(1500); await ctx.close(); }); test("FLOW03 - delete entry workflow", 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("DeleteFlow"); await popup.locator('input[placeholder="domain.com"]').fill("https://deleteflow.com"); await popup.locator('input[placeholder="username or email"]').fill("del@flow.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=DeleteFlow").first().click(); await popup.waitForTimeout(500); const deleteLink = popup.locator("text=delete entry"); await deleteLink.click(); await popup.waitForTimeout(1000); await ctx.close(); }); test("FLOW04 - copy username workflow", 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("CopyUsername"); await popup.locator('input[placeholder="domain.com"]').fill("https://copyuser.com"); await popup.locator('input[placeholder="username or email"]').fill("copyuser@example.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=CopyUsername").first().click(); await popup.waitForTimeout(500); const copyBtn = popup.locator(".peach-copy-btn").first(); await copyBtn.click(); await popup.waitForTimeout(500); await ctx.close(); }); test("FLOW05 - reveal password workflow", 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("RevealPwd"); await popup.locator('input[placeholder="domain.com"]').fill("https://revealpwd.com"); await popup.locator('input[placeholder="username or email"]').fill("reveal@pwd.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("mysecretpassword"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=RevealPwd").first().click(); await popup.waitForTimeout(500); const eyeBtn = popup.locator("button[aria-label='Reveal password']"); await eyeBtn.click(); await popup.waitForTimeout(300); await ctx.close(); }); test("FLOW06 - add entry with TOTP workflow", 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("GitHub2FA"); await popup.locator('input[placeholder="domain.com"]').fill("https://github.com"); await popup.locator('input[placeholder="username or email"]').fill("june@asha.software"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("secret123"); await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await expect(popup.locator("text=GitHub2FA")).toBeVisible({ timeout: 3000 }); await ctx.close(); }); test("FLOW07 - search and find entry", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); for (const name of ["Google", "Amazon", "Microsoft"]) { 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(`user@${name.toLowerCase()}.com`); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.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("Amazon"); await popup.waitForTimeout(300); await expect(popup.locator("text=Amazon")).toBeVisible({ timeout: 3000 }); await expect(popup.locator("text=Google")).not.toBeVisible(); await ctx.close(); }); test("FLOW08 - navigate between entries", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); const pageErrors: string[] = []; popup.on("pageerror", (error: Error) => pageErrors.push(error.message)); await seedEntries(popup, ["First", "Second", "Third"]); await popup.reload(); await popup.waitForTimeout(1500); const unlockInput = popup.locator("#unlock-password"); if (await unlockInput.isVisible().catch(() => false)) { await unlockInput.fill(VAULT_PASSWORD); await popup.getByRole("button", { name: /unlock/i }).click(); await popup.waitForTimeout(1500); } const nextBtn = popup.getByRole("button", { name: /next entry/i }); const shellSearch = popup.locator(".peach-search-wrap .peach-search-input"); const overlay = popup.locator(".peach-overlay-view"); const overlaySearch = popup.locator(".peach-overlay-view .peach-search-input"); const backBtn = popup.locator('.peach-overlay-view button[aria-label="Back"]'); await expect(nextBtn).toBeVisible(); await nextBtn.click(); await shellSearch.click(); await expect(overlay).toBeVisible(); await expect(overlaySearch).toBeVisible(); await expect(popup.locator(".peach-chrome")).toBeHidden(); await expect(popup.locator(".peach-search-wrap")).toBeHidden(); await expect .poll(async () => overlaySearch.evaluate((el: Element) => document.activeElement === el)) .toBe(true); await backBtn.click(); await expect(overlay).toBeHidden(); await expect(popup.locator(".peach-chrome")).toBeVisible(); await expect(popup.locator(".peach-search-wrap")).toBeVisible(); expect(pageErrors).toEqual([]); await popup.waitForTimeout(300); await ctx.close(); }); test("FLOW09 - add entry with notes", 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("NotesEntry"); await popup.locator('input[placeholder="domain.com"]').fill("https://notes.com"); await popup.locator('input[placeholder="username or email"]').fill("notes@test.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); const notesArea = popup.locator("textarea"); await notesArea.fill("This is a secret note with important information."); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await ctx.close(); }); test("FLOW10 - generator and use 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(); expect(pwd1?.length).toBeGreaterThan(0); 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); const useBtn = popup.getByRole("button", { name: /use/i }); await useBtn.click(); await popup.waitForTimeout(500); await ctx.close(); }); }); test.describe("State Transitions", () => { test("STATE01 - empty vault shows correct UI", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); const emptyCopy = popup.locator(".peach-empty-copy, text=first entry"); await expect(emptyCopy).toBeVisible({ timeout: 3000 }); await ctx.close(); }); test("STATE02 - vault with entries shows entry count", 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("CounterTest"); await popup.locator('input[placeholder="domain.com"]').fill("https://counter.com"); await popup.locator('input[placeholder="username or email"]').fill("counter@test.com"); const pwdField = popup.locator(".peach-new-field-input[placeholder='password']"); await pwdField.fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); const counter = popup.locator(".peach-search-counter"); const counterText = await counter.textContent(); expect(counterText).toMatch(/1 \/ 1|1 entries/); await ctx.close(); }); test("STATE03 - settings panel shows all options", 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 expect(popup.locator("text=lock on close")).toBeVisible(); await expect(popup.locator("text=auto-lock")).toBeVisible(); await expect(popup.locator("text=clipboard clear")).toBeVisible(); await ctx.close(); }); test("STATE04 - generator shows toggles", 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"); expect(await toggles.count()).toBeGreaterThan(0); await ctx.close(); }); test("STATE05 - export panel 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[type='password']").first(); await expect(exportPw).toBeVisible(); await ctx.close(); }); test("STATE06 - import panel shows file input", 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']"); await expect(fileInput).toBeAttached(); await ctx.close(); }); test("STATE07 - accent color swatches are clickable", async () => { const ctx = await makeContext(); const { popup } = await setupVault(ctx); await popup.locator(".peach-icon-btn[aria-label='Settings']").click(); await popup.waitForTimeout(500); const swatches = popup.locator(".peach-swatch"); const count = await swatches.count(); expect(count).toBeGreaterThan(0); await swatches.nth(0).click(); await popup.waitForTimeout(200); await swatches.nth(1).click(); await popup.waitForTimeout(200); await ctx.close(); }); test("STATE08 - lock screen shows branding", 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); const wordmark = popup.locator(".wordmark, .peach-wordmark, text=peach"); await expect(wordmark.first()).toBeVisible({ timeout: 3000 }); await ctx.close(); }); });