import { test, expect } from "@playwright/test"; import type { 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-test-${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 createVaultAndAddEntry(ctx: BrowserContext): Promise<{ popup: import("@playwright/test").Page, extId: string }> { const maxWait = 15000; 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`); const extId = new URL(sw.url()).host; const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(2000); const pw = popup.locator("#master-password"); if (await pw.isVisible()) { await pw.fill("testpassword123"); await popup.locator("#confirm-password").fill("testpassword123"); await popup.getByRole("button", { name: /create vault/i }).click(); await popup.waitForTimeout(2000); } return { popup, extId }; } test.describe("Entry CRUD", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("07 - add entry with all fields", async () => { const { popup } = await createVaultAndAddEntry(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@example.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("secret123!"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await expect(popup.locator("text=GitHub").first()).toBeVisible({ timeout: 5000 }); }); test("08 - add entry with TOTP", async () => { const { popup } = await createVaultAndAddEntry(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@example.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("secret123!"); await popup.locator('input[placeholder*="totp" i], input[placeholder*="base32" i]').fill("JBSWY3DPEHPK3PXP"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await expect(popup.locator("text=GitHub2FA").first()).toBeVisible({ timeout: 5000 }); await expect(popup.locator(".peach-totp-code")).toBeVisible({ timeout: 3000 }); }); test("09 - edit entry changes persist", async () => { const { popup } = await createVaultAndAddEntry(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("EditTest"); await popup.locator('input[placeholder="domain.com"]').fill("https://edittest.com"); await popup.locator('input[placeholder="username or email"]').fill("user@edittest.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("oldpass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=EditTest").first().click(); await popup.waitForTimeout(500); const editBtn = popup.locator("button[aria-label='Edit'], .peach-entry-name-button").first(); await editBtn.click(); await popup.waitForTimeout(500); await popup.locator(".peach-new-field-input[placeholder='password']").fill("newpass"); await popup.getByRole("button", { name: /save/i }).click(); await popup.waitForTimeout(1500); await popup.locator('input[placeholder="search vault..."]').click(); await popup.waitForTimeout(500); await popup.locator("text=EditTest").first().click(); await popup.waitForTimeout(500); await expect(popup.locator("text=newpass").first()).toBeVisible({ timeout: 3000 }); }); test("10 - delete entry removes from vault", async () => { const { popup } = await createVaultAndAddEntry(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("DeleteMe"); await popup.locator('input[placeholder="domain.com"]').fill("https://deleteme.com"); await popup.locator('input[placeholder="username or email"]').fill("del@ete.me"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("deletepass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(1500); await popup.locator("text=DeleteMe").first().click(); await popup.waitForTimeout(500); await popup.locator("text=delete entry").click(); await popup.waitForTimeout(1000); await expect(popup.locator("text=DeleteMe").first()).not.toBeVisible(); }); test("11 - search filters entries by title", async () => { const { popup } = await createVaultAndAddEntry(ctx); for (const name of ["Apple", "Amazon", "Google"]) { 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(`u@${name.toLowerCase()}.com`); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(800); } await popup.locator('input[placeholder="search vault..."]').click(); await popup.waitForTimeout(300); const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first(); await searchInput.fill("Apple"); await popup.waitForTimeout(500); await expect(popup.locator("text=Apple")).toBeVisible(); await expect(popup.locator("text=Amazon")).not.toBeVisible(); }); test("12 - prev/next navigation cycles through entries", async () => { const { popup } = await createVaultAndAddEntry(ctx); for (const name of ["FirstEntry", "SecondEntry"]) { 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@test.com"); await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass"); await popup.getByRole("button", { name: /create/i }).click(); await popup.waitForTimeout(800); } const nextBtn = popup.getByRole("button", { name: /next/i }); await nextBtn.click(); await popup.waitForTimeout(500); const content = popup.locator(".peach-content-frame, .peach-stage").first(); await expect(content).toBeVisible(); }); test("13 - empty vault shows empty state", async () => { const { popup } = await createVaultAndAddEntry(ctx); await expect(popup.locator(".peach-empty-copy, .peach-entry-name, text=empty vault").first()).toBeVisible({ timeout: 3000 }); }); });