1import { test, expect, BrowserContext } from "@playwright/test";2import { fileURLToPath } from "url";3import { dirname, join } from "path";4import os from "os";5import fs from "fs/promises";67const __filename = fileURLToPath(import.meta.url);8const __dirname = dirname(__filename);9const EXTENSION_PATH = join(__dirname, "..", "dist");10const VAULT_PASSWORD = "CorrectHorseBattery42!";1112async function makeContext() {13 const { chromium } = await import("playwright");14 const userDataDir = join(os.tmpdir(), `peach-entries-${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 waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {23 const start = Date.now();24 let sw = ctx.serviceWorkers()[0];25 while (!sw && Date.now() - start < maxWait) {26 await new Promise(r => setTimeout(r, 500));27 sw = ctx.serviceWorkers()[0];28 }29 if (!sw) throw new Error(`No SW after ${maxWait}ms`);30 return new URL(sw.url()).host;31}3233async function setupVault(ctx: BrowserContext): Promise<{ popup: any, extId: string }> {34 const extId = await waitForSW(ctx);35 const popup = await ctx.newPage();36 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);37 await popup.waitForTimeout(3000);38 const pw = popup.locator("#master-password");39 if (await pw.isVisible()) {40 await pw.fill(VAULT_PASSWORD);41 await popup.locator("#confirm-password").fill(VAULT_PASSWORD);42 await popup.waitForFunction(() => {43 const submit = document.querySelector(".peach-lock-submit");44 return submit instanceof HTMLButtonElement && !submit.disabled;45 });46 await popup.getByRole("button", { name: /create vault/i }).click();47 await popup.waitForTimeout(2000);48 }49 return { popup, extId };50}5152async function seedEntries(popup: any, names: string[]) {53 for (const name of names) {54 await popup.evaluate(async (entryName: string) => {55 await new Promise((resolve) => {56 chrome.runtime.sendMessage(57 {58 type: "ADD_ENTRY",59 payload: {60 type: "login",61 title: entryName,62 url: `https://${entryName.toLowerCase()}.com`,63 username: `${entryName.toLowerCase()}@test.com`,64 password: "Passphrase123!",65 },66 },67 resolve68 );69 });70 }, name);71 }72}7374test.describe("Entry Interaction Flows", () => {7576 test("FLOW01 - full add entry workflow", async () => {77 const ctx = await makeContext();78 const { popup } = await setupVault(ctx);79 await popup.locator('input[placeholder="search vault..."]').click();80 await popup.getByRole("button", { name: /new entry/i }).click();81 await popup.locator(".peach-new-name-input").fill("GitHub");82 await popup.locator('input[placeholder="domain.com"]').fill("https://github.com");83 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");84 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");85 await pwdField.fill("supersecret123");86 await popup.getByRole("button", { name: /create/i }).click();87 await popup.waitForTimeout(1500);88 await expect(popup.locator("text=GitHub")).toBeVisible({ timeout: 3000 });89 await ctx.close();90 });9192 test("FLOW02 - edit entry workflow", async () => {93 const ctx = await makeContext();94 const { popup } = await setupVault(ctx);95 await popup.locator('input[placeholder="search vault..."]').click();96 await popup.getByRole("button", { name: /new entry/i }).click();97 await popup.locator(".peach-new-name-input").fill("EditFlow");98 await popup.locator('input[placeholder="domain.com"]').fill("https://editflow.com");99 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");100 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");101 await pwdField.fill("oldpass");102 await popup.getByRole("button", { name: /create/i }).click();103 await popup.waitForTimeout(1500);104 await popup.locator("text=EditFlow").first().click();105 await popup.waitForTimeout(500);106 const editBtn = popup.locator("button[aria-label='Edit']").first();107 await editBtn.click();108 await popup.waitForTimeout(500);109 const newPwdField = popup.locator(".peach-new-field-input[placeholder='password']");110 await newPwdField.fill("newpass");111 await popup.getByRole("button", { name: /save/i }).click();112 await popup.waitForTimeout(1500);113 await ctx.close();114 });115116 test("FLOW03 - delete entry workflow", async () => {117 const ctx = await makeContext();118 const { popup } = await setupVault(ctx);119 await popup.locator('input[placeholder="search vault..."]').click();120 await popup.getByRole("button", { name: /new entry/i }).click();121 await popup.locator(".peach-new-name-input").fill("DeleteFlow");122 await popup.locator('input[placeholder="domain.com"]').fill("https://deleteflow.com");123 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");124 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");125 await pwdField.fill("pass123");126 await popup.getByRole("button", { name: /create/i }).click();127 await popup.waitForTimeout(1500);128 await popup.locator("text=DeleteFlow").first().click();129 await popup.waitForTimeout(500);130 const deleteLink = popup.locator("text=delete entry");131 await deleteLink.click();132 await popup.waitForTimeout(1000);133 await ctx.close();134 });135136 test("FLOW04 - copy username workflow", async () => {137 const ctx = await makeContext();138 const { popup } = await setupVault(ctx);139 await popup.locator('input[placeholder="search vault..."]').click();140 await popup.getByRole("button", { name: /new entry/i }).click();141 await popup.locator(".peach-new-name-input").fill("CopyUsername");142 await popup.locator('input[placeholder="domain.com"]').fill("https://copyuser.com");143 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");144 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");145 await pwdField.fill("pass123");146 await popup.getByRole("button", { name: /create/i }).click();147 await popup.waitForTimeout(1500);148 await popup.locator("text=CopyUsername").first().click();149 await popup.waitForTimeout(500);150 const copyBtn = popup.locator(".peach-copy-btn").first();151 await copyBtn.click();152 await popup.waitForTimeout(500);153 await ctx.close();154 });155156 test("FLOW05 - reveal password workflow", async () => {157 const ctx = await makeContext();158 const { popup } = await setupVault(ctx);159 await popup.locator('input[placeholder="search vault..."]').click();160 await popup.getByRole("button", { name: /new entry/i }).click();161 await popup.locator(".peach-new-name-input").fill("RevealPwd");162 await popup.locator('input[placeholder="domain.com"]').fill("https://revealpwd.com");163 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");164 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");165 await pwdField.fill("mysecretpassword");166 await popup.getByRole("button", { name: /create/i }).click();167 await popup.waitForTimeout(1500);168 await popup.locator("text=RevealPwd").first().click();169 await popup.waitForTimeout(500);170 const eyeBtn = popup.locator("button[aria-label='Reveal password']");171 await eyeBtn.click();172 await popup.waitForTimeout(300);173 await ctx.close();174 });175176 test("FLOW06 - add entry with TOTP workflow", async () => {177 const ctx = await makeContext();178 const { popup } = await setupVault(ctx);179 await popup.locator('input[placeholder="search vault..."]').click();180 await popup.getByRole("button", { name: /new entry/i }).click();181 await popup.locator(".peach-new-name-input").fill("GitHub2FA");182 await popup.locator('input[placeholder="domain.com"]').fill("https://github.com");183 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");184 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");185 await pwdField.fill("secret123");186 await popup.locator("input[placeholder*='totp' i]").fill("JBSWY3DPEHPK3PXP");187 await popup.getByRole("button", { name: /create/i }).click();188 await popup.waitForTimeout(1500);189 await expect(popup.locator("text=GitHub2FA")).toBeVisible({ timeout: 3000 });190 await ctx.close();191 });192193 test("FLOW07 - search and find entry", async () => {194 const ctx = await makeContext();195 const { popup } = await setupVault(ctx);196 for (const name of ["Google", "Amazon", "Microsoft"]) {197 await popup.locator('input[placeholder="search vault..."]').click();198 await popup.getByRole("button", { name: /new entry/i }).click();199 await popup.locator(".peach-new-name-input").fill(name);200 await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);201 await popup.locator('input[placeholder="username or email"]').fill(`user@${name.toLowerCase()}.com`);202 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");203 await pwdField.fill("pass123");204 await popup.getByRole("button", { name: /create/i }).click();205 await popup.waitForTimeout(500);206 }207 const searchInput = popup.locator(".peach-search-input, .peach-search-input-list").first();208 await searchInput.click();209 await searchInput.fill("Amazon");210 await popup.waitForTimeout(300);211 await expect(popup.locator("text=Amazon")).toBeVisible({ timeout: 3000 });212 await expect(popup.locator("text=Google")).not.toBeVisible();213 await ctx.close();214 });215216 test("FLOW08 - navigate between entries", async () => {217 const ctx = await makeContext();218 const { popup } = await setupVault(ctx);219 const pageErrors: string[] = [];220 popup.on("pageerror", (error: Error) => pageErrors.push(error.message));221222 await seedEntries(popup, ["First", "Second", "Third"]);223 await popup.reload();224 await popup.waitForTimeout(1500);225226 const unlockInput = popup.locator("#unlock-password");227 if (await unlockInput.isVisible().catch(() => false)) {228 await unlockInput.fill(VAULT_PASSWORD);229 await popup.getByRole("button", { name: /unlock/i }).click();230 await popup.waitForTimeout(1500);231 }232233 const nextBtn = popup.getByRole("button", { name: /next entry/i });234 const shellSearch = popup.locator(".peach-search-wrap .peach-search-input");235 const overlay = popup.locator(".peach-overlay-view");236 const overlaySearch = popup.locator(".peach-overlay-view .peach-search-input");237 const backBtn = popup.locator('.peach-overlay-view button[aria-label="Back"]');238239 await expect(nextBtn).toBeVisible();240 await nextBtn.click();241 await shellSearch.click();242 await expect(overlay).toBeVisible();243 await expect(overlaySearch).toBeVisible();244 await expect(popup.locator(".peach-chrome")).toBeHidden();245 await expect(popup.locator(".peach-search-wrap")).toBeHidden();246 await expect247 .poll(async () => overlaySearch.evaluate((el: Element) => document.activeElement === el))248 .toBe(true);249 await backBtn.click();250 await expect(overlay).toBeHidden();251 await expect(popup.locator(".peach-chrome")).toBeVisible();252 await expect(popup.locator(".peach-search-wrap")).toBeVisible();253 expect(pageErrors).toEqual([]);254255 await popup.waitForTimeout(300);256 await ctx.close();257 });258259 test("FLOW09 - add entry with notes", async () => {260 const ctx = await makeContext();261 const { popup } = await setupVault(ctx);262 await popup.locator('input[placeholder="search vault..."]').click();263 await popup.getByRole("button", { name: /new entry/i }).click();264 await popup.locator(".peach-new-name-input").fill("NotesEntry");265 await popup.locator('input[placeholder="domain.com"]').fill("https://notes.com");266 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");267 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");268 await pwdField.fill("pass123");269 const notesArea = popup.locator("textarea");270 await notesArea.fill("This is a secret note with important information.");271 await popup.getByRole("button", { name: /create/i }).click();272 await popup.waitForTimeout(1500);273 await ctx.close();274 });275276 test("FLOW10 - generator and use password", async () => {277 const ctx = await makeContext();278 const { popup } = await setupVault(ctx);279 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();280 await popup.waitForTimeout(300);281 await popup.getByRole("button", { name: /password generator/i }).click();282 await popup.waitForTimeout(500);283 const pwd1 = await popup.locator(".peach-pwgen-output").textContent();284 expect(pwd1?.length).toBeGreaterThan(0);285 await popup.locator("button[aria-label='Regenerate']").click();286 await popup.waitForTimeout(300);287 const pwd2 = await popup.locator(".peach-pwgen-output").textContent();288 expect(pwd1).not.toEqual(pwd2);289 const useBtn = popup.getByRole("button", { name: /use/i });290 await useBtn.click();291 await popup.waitForTimeout(500);292 await ctx.close();293 });294});295296test.describe("State Transitions", () => {297298 test("STATE01 - empty vault shows correct UI", async () => {299 const ctx = await makeContext();300 const { popup } = await setupVault(ctx);301 const emptyCopy = popup.locator(".peach-empty-copy, text=first entry");302 await expect(emptyCopy).toBeVisible({ timeout: 3000 });303 await ctx.close();304 });305306 test("STATE02 - vault with entries shows entry count", async () => {307 const ctx = await makeContext();308 const { popup } = await setupVault(ctx);309 await popup.locator('input[placeholder="search vault..."]').click();310 await popup.getByRole("button", { name: /new entry/i }).click();311 await popup.locator(".peach-new-name-input").fill("CounterTest");312 await popup.locator('input[placeholder="domain.com"]').fill("https://counter.com");313 await popup.locator('input[placeholder="username or email"]').fill("[email protected]");314 const pwdField = popup.locator(".peach-new-field-input[placeholder='password']");315 await pwdField.fill("pass123");316 await popup.getByRole("button", { name: /create/i }).click();317 await popup.waitForTimeout(1500);318 const counter = popup.locator(".peach-search-counter");319 const counterText = await counter.textContent();320 expect(counterText).toMatch(/1 \/ 1|1 entries/);321 await ctx.close();322 });323324 test("STATE03 - settings panel shows all options", async () => {325 const ctx = await makeContext();326 const { popup } = await setupVault(ctx);327 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();328 await popup.waitForTimeout(500);329 await expect(popup.locator("text=lock on close")).toBeVisible();330 await expect(popup.locator("text=auto-lock")).toBeVisible();331 await expect(popup.locator("text=clipboard clear")).toBeVisible();332 await ctx.close();333 });334335 test("STATE04 - generator shows toggles", async () => {336 const ctx = await makeContext();337 const { popup } = await setupVault(ctx);338 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();339 await popup.waitForTimeout(300);340 await popup.getByRole("button", { name: /password generator/i }).click();341 await popup.waitForTimeout(500);342 const toggles = popup.locator(".peach-pwgen-toggle-row");343 expect(await toggles.count()).toBeGreaterThan(0);344 await ctx.close();345 });346347 test("STATE05 - export panel requires password", async () => {348 const ctx = await makeContext();349 const { popup } = await setupVault(ctx);350 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();351 await popup.waitForTimeout(300);352 await popup.getByRole("button", { name: /export/i }).click();353 await popup.waitForTimeout(500);354 const exportPw = popup.locator("input[type='password']").first();355 await expect(exportPw).toBeVisible();356 await ctx.close();357 });358359 test("STATE06 - import panel shows file input", async () => {360 const ctx = await makeContext();361 const { popup } = await setupVault(ctx);362 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();363 await popup.waitForTimeout(300);364 await popup.getByRole("button", { name: /import/i }).click();365 await popup.waitForTimeout(500);366 const fileInput = popup.locator("input[type='file']");367 await expect(fileInput).toBeAttached();368 await ctx.close();369 });370371 test("STATE07 - accent color swatches are clickable", async () => {372 const ctx = await makeContext();373 const { popup } = await setupVault(ctx);374 await popup.locator(".peach-icon-btn[aria-label='Settings']").click();375 await popup.waitForTimeout(500);376 const swatches = popup.locator(".peach-swatch");377 const count = await swatches.count();378 expect(count).toBeGreaterThan(0);379 await swatches.nth(0).click();380 await popup.waitForTimeout(200);381 await swatches.nth(1).click();382 await popup.waitForTimeout(200);383 await ctx.close();384 });385386 test("STATE08 - lock screen shows branding", async () => {387 const ctx = await makeContext();388 const extId = await waitForSW(ctx);389 const popup = await ctx.newPage();390 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);391 await popup.waitForTimeout(2000);392 const wordmark = popup.locator(".wordmark, .peach-wordmark, text=peach");393 await expect(wordmark.first()).toBeVisible({ timeout: 3000 });394 await ctx.close();395 });396});