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 setupVault(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("password123");38 await popup.locator("#confirm-password").fill("password123");39 await popup.getByRole("button", { name: /create vault/i }).click();40 await popup.waitForTimeout(2000);41 }42 return { popup, extId };43}4445test.describe("Copy & TOTP", () => {46 let ctx: BrowserContext;47 test.afterEach(async () => { await ctx.close(); });48 test.beforeEach(async () => { ctx = await makeContext(); });4950 test("14 - copy username shows copied state", async () => {51 const { popup } = await setupVault(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("CopyTest");55 await popup.locator('input[placeholder="domain.com"]').fill("https://copytest.com");56 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");57 await popup.locator(".peach-new-field-input[placeholder='password']").fill("copypass");58 await popup.getByRole("button", { name: /create/i }).click();59 await popup.waitForTimeout(1500);60 await popup.locator("text=CopyTest").first().click();61 await popup.waitForTimeout(500);62 const copyBtn = popup.locator(".peach-copy-btn").first();63 await copyBtn.click();64 await popup.waitForTimeout(300);65 await expect(copyBtn).toHaveClass(/is-copied/);66 });6768 test("15 - password reveal toggles visibility", async () => {69 const { popup } = await setupVault(ctx);70 await popup.locator('input[placeholder="search vault..."]').click();71 await popup.getByRole("button", { name: /new entry/i }).click();72 await popup.locator(".peach-new-name-input").fill("RevealTest");73 await popup.locator('input[placeholder="domain.com"]').fill("https://revealtest.com");74 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");75 await popup.locator(".peach-new-field-input[placeholder='password']").fill("secretpassword");76 await popup.getByRole("button", { name: /create/i }).click();77 await popup.waitForTimeout(1500);78 await popup.locator("text=RevealTest").first().click();79 await popup.waitForTimeout(500);80 await expect(popup.locator(".peach-password-dots")).toBeVisible();81 const eyeBtn = popup.locator("button[aria-label='Reveal password']");82 await eyeBtn.click();83 await popup.waitForTimeout(300);84 });8586 test("16 - TOTP countdown ring animates", async () => {87 const { popup } = await setupVault(ctx);88 await popup.locator('input[placeholder="search vault..."]').click();89 await popup.getByRole("button", { name: /new entry/i }).click();90 await popup.locator(".peach-new-name-input").fill("TOTPTest");91 await popup.locator('input[placeholder="domain.com"]').fill("https://totptest.com");92 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");93 await popup.locator(".peach-new-field-input[placeholder='password']").fill("totppass");94 await popup.locator('input[placeholder*="totp" i]').fill("JBSWY3DPEHPK3PXP");95 await popup.getByRole("button", { name: /create/i }).click();96 await popup.waitForTimeout(1500);97 await popup.locator("text=TOTPTest").first().click();98 await popup.waitForTimeout(500);99 const ring = popup.locator(".peach-totp-ring-progress, svg circle").first();100 await expect(ring).toBeVisible();101 });102103 test("17 - generator produces different passwords each time", async () => {104 const { popup } = await setupVault(ctx);105 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();106 await popup.waitForTimeout(300);107 await popup.getByRole("button", { name: /password generator/i }).click();108 await popup.waitForTimeout(500);109 const output1 = await popup.locator(".peach-pwgen-output").textContent();110 await popup.locator("button[aria-label='Regenerate']").click();111 await popup.waitForTimeout(300);112 const output2 = await popup.locator(".peach-pwgen-output").textContent();113 expect(output1).not.toEqual(output2);114 });115116 test("18 - generator respects length slider", async () => {117 const { popup } = await setupVault(ctx);118 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();119 await popup.waitForTimeout(300);120 await popup.getByRole("button", { name: /password generator/i }).click();121 await popup.waitForTimeout(500);122 const slider = popup.locator(".peach-pwgen-slider");123 await slider.fill("32");124 await popup.waitForTimeout(300);125 const output = await popup.locator(".peach-pwgen-output").textContent();126 expect(output?.length).toBeGreaterThanOrEqual(30);127 });128129 test("19 - settings accent color swatches update --ac", async () => {130 const { popup } = await setupVault(ctx);131 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();132 await popup.waitForTimeout(500);133 const swatch = popup.locator(".peach-swatch").nth(2);134 await swatch.click();135 await popup.waitForTimeout(300);136 await expect(swatch).toHaveClass(/is-active/);137 });138139 test("20 - clipboard clear after 30 seconds", async () => {140 const { popup } = await setupVault(ctx);141 await popup.locator('input[placeholder="search vault..."]').click();142 await popup.getByRole("button", { name: /new entry/i }).click();143 await popup.locator(".peach-new-name-input").fill("ClipboardTest");144 await popup.locator('input[placeholder="domain.com"]').fill("https://clipboardtest.com");145 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");146 await popup.locator(".peach-new-field-input[placeholder='password']").fill("cleartest");147 await popup.getByRole("button", { name: /create/i }).click();148 await popup.waitForTimeout(1500);149 await popup.locator("text=ClipboardTest").first().click();150 await popup.waitForTimeout(500);151 await popup.locator(".peach-copy-btn").nth(1).click();152 await popup.waitForTimeout(200);153 const copyBtns = popup.locator(".peach-copy-btn.is-copied");154 expect(await copyBtns.count()).toBeGreaterThan(0);155 });156});