entry-flows-state.spec.ts

17.4 KB · raw

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});