1import { test, expect } from "@playwright/test";2import type { BrowserContext } from "@playwright/test";3import { fileURLToPath } from "url";4import { dirname, join } from "path";5import os from "os";6import fs from "fs/promises";78const __filename = fileURLToPath(import.meta.url);9const __dirname = dirname(__filename);10const EXTENSION_PATH = join(__dirname, "..", "dist");1112async function makeContext() {13 const { chromium } = await import("playwright");14 const userDataDir = join(os.tmpdir(), `peach-test-${Date.now()}`);15 await fs.mkdir(userDataDir, { recursive: true });16 return chromium.launchPersistentContext(userDataDir, {17 headless: false,18 args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],19 });20}2122async function createVaultAndAddEntry(ctx: BrowserContext): Promise<{ popup: import("@playwright/test").Page, extId: string }> {23 const maxWait = 15000;24 const start = Date.now();25 let sw = ctx.serviceWorkers()[0];26 while (!sw && Date.now() - start < maxWait) {27 await new Promise(r => setTimeout(r, 500));28 sw = ctx.serviceWorkers()[0];29 }30 if (!sw) throw new Error(`No SW after ${maxWait}ms`);31 const extId = new URL(sw.url()).host;32 const popup = await ctx.newPage();33 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);34 await popup.waitForTimeout(2000);35 const pw = popup.locator("#master-password");36 if (await pw.isVisible()) {37 await pw.fill("testpassword123");38 await popup.locator("#confirm-password").fill("testpassword123");39 await popup.getByRole("button", { name: /create vault/i }).click();40 await popup.waitForTimeout(2000);41 }42 return { popup, extId };43}4445test.describe("Entry CRUD", () => {46 let ctx: BrowserContext;47 test.afterEach(async () => { await ctx.close(); });48 test.beforeEach(async () => { ctx = await makeContext(); });4950 test("07 - add entry with all fields", async () => {51 const { popup } = await createVaultAndAddEntry(ctx);52 await popup.locator('input[placeholder="search vault..."]').click();53 await popup.getByRole("button", { name: /new entry/i }).click();54 await popup.locator(".peach-new-name-input").fill("GitHub");55 await popup.locator('input[placeholder="domain.com"]').fill("https://github.com");56 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");57 await popup.locator(".peach-new-field-input[placeholder='password']").fill("secret123!");58 await popup.getByRole("button", { name: /create/i }).click();59 await popup.waitForTimeout(1500);60 await expect(popup.locator("text=GitHub").first()).toBeVisible({ timeout: 5000 });61 });6263 test("08 - add entry with TOTP", async () => {64 const { popup } = await createVaultAndAddEntry(ctx);65 await popup.locator('input[placeholder="search vault..."]').click();66 await popup.getByRole("button", { name: /new entry/i }).click();67 await popup.locator(".peach-new-name-input").fill("GitHub2FA");68 await popup.locator('input[placeholder="domain.com"]').fill("https://github.com");69 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");70 await popup.locator(".peach-new-field-input[placeholder='password']").fill("secret123!");71 await popup.locator('input[placeholder*="totp" i], input[placeholder*="base32" i]').fill("JBSWY3DPEHPK3PXP");72 await popup.getByRole("button", { name: /create/i }).click();73 await popup.waitForTimeout(1500);74 await expect(popup.locator("text=GitHub2FA").first()).toBeVisible({ timeout: 5000 });75 await expect(popup.locator(".peach-totp-code")).toBeVisible({ timeout: 3000 });76 });7778 test("09 - edit entry changes persist", async () => {79 const { popup } = await createVaultAndAddEntry(ctx);80 await popup.locator('input[placeholder="search vault..."]').click();81 await popup.getByRole("button", { name: /new entry/i }).click();82 await popup.locator(".peach-new-name-input").fill("EditTest");83 await popup.locator('input[placeholder="domain.com"]').fill("https://edittest.com");84 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");85 await popup.locator(".peach-new-field-input[placeholder='password']").fill("oldpass");86 await popup.getByRole("button", { name: /create/i }).click();87 await popup.waitForTimeout(1500);88 await popup.locator("text=EditTest").first().click();89 await popup.waitForTimeout(500);90 const editBtn = popup.locator("button[aria-label='Edit'], .peach-entry-name-button").first();91 await editBtn.click();92 await popup.waitForTimeout(500);93 await popup.locator(".peach-new-field-input[placeholder='password']").fill("newpass");94 await popup.getByRole("button", { name: /save/i }).click();95 await popup.waitForTimeout(1500);96 await popup.locator('input[placeholder="search vault..."]').click();97 await popup.waitForTimeout(500);98 await popup.locator("text=EditTest").first().click();99 await popup.waitForTimeout(500);100 await expect(popup.locator("text=newpass").first()).toBeVisible({ timeout: 3000 });101 });102103 test("10 - delete entry removes from vault", async () => {104 const { popup } = await createVaultAndAddEntry(ctx);105 await popup.locator('input[placeholder="search vault..."]').click();106 await popup.getByRole("button", { name: /new entry/i }).click();107 await popup.locator(".peach-new-name-input").fill("DeleteMe");108 await popup.locator('input[placeholder="domain.com"]').fill("https://deleteme.com");109 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");110 await popup.locator(".peach-new-field-input[placeholder='password']").fill("deletepass");111 await popup.getByRole("button", { name: /create/i }).click();112 await popup.waitForTimeout(1500);113 await popup.locator("text=DeleteMe").first().click();114 await popup.waitForTimeout(500);115 await popup.locator("text=delete entry").click();116 await popup.waitForTimeout(1000);117 await expect(popup.locator("text=DeleteMe").first()).not.toBeVisible();118 });119120 test("11 - search filters entries by title", async () => {121 const { popup } = await createVaultAndAddEntry(ctx);122 for (const name of ["Apple", "Amazon", "Google"]) {123 await popup.locator('input[placeholder="search vault..."]').click();124 await popup.getByRole("button", { name: /new entry/i }).click();125 await popup.locator(".peach-new-name-input").fill(name);126 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);127 await popup.locator('input[placeholder="username or email"]').fill(`u@${name.toLowerCase()}.com`);128 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass123");129 await popup.getByRole("button", { name: /create/i }).click();130 await popup.waitForTimeout(800);131 }132 await popup.locator('input[placeholder="search vault..."]').click();133 await popup.waitForTimeout(300);134 const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();135 await searchInput.fill("Apple");136 await popup.waitForTimeout(500);137 await expect(popup.locator("text=Apple")).toBeVisible();138 await expect(popup.locator("text=Amazon")).not.toBeVisible();139 });140141 test("12 - prev/next navigation cycles through entries", async () => {142 const { popup } = await createVaultAndAddEntry(ctx);143 for (const name of ["FirstEntry", "SecondEntry"]) {144 await popup.locator('input[placeholder="search vault..."]').click();145 await popup.getByRole("button", { name: /new entry/i }).click();146 await popup.locator(".peach-new-name-input").fill(name);147 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);148 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");149 await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");150 await popup.getByRole("button", { name: /create/i }).click();151 await popup.waitForTimeout(800);152 }153 const nextBtn = popup.getByRole("button", { name: /next/i });154 await nextBtn.click();155 await popup.waitForTimeout(500);156 const content = popup.locator(".peach-content-frame, .peach-stage").first();157 await expect(content).toBeVisible();158 });159160 test("13 - empty vault shows empty state", async () => {161 const { popup } = await createVaultAndAddEntry(ctx);162 await expect(popup.locator(".peach-empty-copy, .peach-entry-name, text=empty vault").first()).toBeVisible({ timeout: 3000 });163 });164});