entry-comprehensive.spec.ts

35.0 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");1011async function makeContext() {12  const { chromium } = await import("playwright");13  const userDataDir = join(os.tmpdir(), `peach-entry-${Date.now()}`);14  await fs.mkdir(userDataDir, { recursive: true });15  return chromium.launchPersistentContext(userDataDir, {16    headless: false,17    args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],18  });19}2021async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {22  const start = Date.now();23  let sw = ctx.serviceWorkers()[0];24  while (!sw && Date.now() - start < maxWait) {25    await new Promise(r => setTimeout(r, 500));26    sw = ctx.serviceWorkers()[0];27  }28  if (!sw) throw new Error(`No SW after ${maxWait}ms`);29  return new URL(sw.url()).host;30}3132async function setupVault(ctx: BrowserContext): Promise<{ popup: any, extId: string }> {33  const extId = await waitForSW(ctx);34  const popup = await ctx.newPage();35  await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);36  await popup.waitForTimeout(3000);37  const pw = popup.locator("#master-password");38  if (await pw.isVisible()) {39    await pw.fill("password123");40    await popup.locator("#confirm-password").fill("password123");41    await popup.getByRole("button", { name: /create vault/i }).click();42    await popup.waitForTimeout(2000);43  }44  return { popup, extId };45}4647test.describe("Entry Field Validation", () => {48  let ctx: BrowserContext;49  test.afterEach(async () => { await ctx.close(); });50  test.beforeEach(async () => { ctx = await makeContext(); });5152  test("EF01 - entry requires name", async () => {53    const { popup } = await setupVault(ctx);54    await popup.locator('input[placeholder="search vault..."]').click();55    await popup.getByRole("button", { name: /new entry/i }).click();56    await popup.locator('input[placeholder="domain.com"]').fill("https://noname.com");57    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");58    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");59    await popup.getByRole("button", { name: /create/i }).click();60    await popup.waitForTimeout(500);61    const nameInput = popup.locator(".peach-new-name-input");62    const value = await nameInput.inputValue();63    expect(value).toBe("");64    await ctx.close();65  });6667  test("EF02 - entry allows empty domain", async () => {68    const { popup } = await setupVault(ctx);69    await popup.locator('input[placeholder="search vault..."]').click();70    await popup.getByRole("button", { name: /new entry/i }).click();71    await popup.locator(".peach-new-name-input").fill("NoDomain");72    await popup.locator('input[placeholder="domain.com"]').fill("");73    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");74    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");75    await popup.getByRole("button", { name: /create/i }).click();76    await popup.waitForTimeout(1500);77    await expect(popup.locator("text=NoDomain")).toBeVisible({ timeout: 3000 });78    await ctx.close();79  });8081  test("EF03 - entry allows empty username", async () => {82    const { popup } = await setupVault(ctx);83    await popup.locator('input[placeholder="search vault..."]').click();84    await popup.getByRole("button", { name: /new entry/i }).click();85    await popup.locator(".peach-new-name-input").fill("NoUsername");86    await popup.locator('input[placeholder="domain.com"]').fill("https://nouser.com");87    await popup.locator('input[placeholder="username or email"]').fill("");88    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");89    await popup.getByRole("button", { name: /create/i }).click();90    await popup.waitForTimeout(1500);91    await expect(popup.locator("text=NoUsername")).toBeVisible({ timeout: 3000 });92    await ctx.close();93  });9495  test("EF04 - entry requires password", async () => {96    const { popup } = await setupVault(ctx);97    await popup.locator('input[placeholder="search vault..."]').click();98    await popup.getByRole("button", { name: /new entry/i }).click();99    await popup.locator(".peach-new-name-input").fill("NoPass");100    await popup.locator('input[placeholder="domain.com"]').fill("https://nopass.com");101    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");102    await popup.getByRole("button", { name: /create/i }).click();103    await popup.waitForTimeout(500);104    const passField = popup.locator(".peach-new-field-input[placeholder='password']");105    const value = await passField.inputValue();106    expect(value).toBe("");107    await ctx.close();108  });109110  test("EF05 - entry truncates very long names", async () => {111    const { popup } = await setupVault(ctx);112    await popup.locator('input[placeholder="search vault..."]').click();113    await popup.getByRole("button", { name: /new entry/i }).click();114    const longName = "A".repeat(500);115    await popup.locator(".peach-new-name-input").fill(longName);116    await popup.locator('input[placeholder="domain.com"]').fill("https://long.com");117    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");118    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");119    await popup.getByRole("button", { name: /create/i }).click();120    await popup.waitForTimeout(1500);121    const nameInList = popup.locator(".peach-entry-name").first();122    const text = await nameInList.textContent();123    expect(text?.length).toBeLessThanOrEqual(200);124    await ctx.close();125  });126});127128test.describe("Notes Field", () => {129  let ctx: BrowserContext;130  test.afterEach(async () => { await ctx.close(); });131  test.beforeEach(async () => { ctx = await makeContext(); });132133  test("EF06 - notes field accepts multiline text", async () => {134    const { popup } = await setupVault(ctx);135    await popup.locator('input[placeholder="search vault..."]').click();136    await popup.getByRole("button", { name: /new entry/i }).click();137    await popup.locator(".peach-new-name-input").fill("MultiLine");138    await popup.locator('input[placeholder="domain.com"]').fill("https://multi.com");139    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");140    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");141    const notes = popup.locator("textarea");142    await notes.fill("Line 1\nLine 2\nLine 3");143    await popup.getByRole("button", { name: /create/i }).click();144    await popup.waitForTimeout(1500);145    await popup.locator(".peach-entry-name-button").first().click();146    await popup.waitForTimeout(500);147    const noteContent = popup.locator(".peach-note-content");148    await expect(noteContent).toBeVisible({ timeout: 3000 });149    const text = await noteContent.textContent();150    expect(text).toContain("Line 1");151    expect(text).toContain("Line 2");152    await ctx.close();153  });154155  test("EF07 - notes field accepts special characters", async () => {156    const { popup } = await setupVault(ctx);157    await popup.locator('input[placeholder="search vault..."]').click();158    await popup.getByRole("button", { name: /new entry/i }).click();159    await popup.locator(".peach-new-name-input").fill("SpecialNotes");160    await popup.locator('input[placeholder="domain.com"]').fill("https://special.com");161    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");162    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");163    const notes = popup.locator("textarea");164    await notes.fill('<script>alert("xss")</script>&"\'<>@#$%^&*()_+-=[]{}|;:,./<>?');165    await popup.getByRole("button", { name: /create/i }).click();166    await popup.waitForTimeout(1500);167    await popup.locator(".peach-entry-name-button").first().click();168    await popup.waitForTimeout(500);169    const noteContent = popup.locator(".peach-note-content");170    const html = await noteContent.innerHTML();171    expect(html).not.toContain("<script>");172    await ctx.close();173  });174175  test("EF08 - notes field supports unicode", async () => {176    const { popup } = await setupVault(ctx);177    await popup.locator('input[placeholder="search vault..."]').click();178    await popup.getByRole("button", { name: /new entry/i }).click();179    await popup.locator(".peach-new-name-input").fill("UnicodeNotes");180    await popup.locator('input[placeholder="domain.com"]').fill("https://unicode.com");181    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");182    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");183    const notes = popup.locator("textarea");184    await notes.fill("日本語한국어中文العربيةहिन्दी🎉🎊💻⌨️");185    await popup.getByRole("button", { name: /create/i }).click();186    await popup.waitForTimeout(1500);187    await expect(popup.locator("text=UnicodeNotes")).toBeVisible({ timeout: 3000 });188    await ctx.close();189  });190191  test("EF09 - very long notes are saved", async () => {192    const { popup } = await setupVault(ctx);193    await popup.locator('input[placeholder="search vault..."]').click();194    await popup.getByRole("button", { name: /new entry/i }).click();195    await popup.locator(".peach-new-name-input").fill("LongNotes");196    await popup.locator('input[placeholder="domain.com"]').fill("https://longnotes.com");197    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");198    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");199    const notes = popup.locator("textarea");200    const longText = "A".repeat(5000);201    await notes.fill(longText);202    await popup.getByRole("button", { name: /create/i }).click();203    await popup.waitForTimeout(1500);204    await popup.locator(".peach-entry-name-button").first().click();205    await popup.waitForTimeout(500);206    const noteContent = popup.locator(".peach-note-content");207    const text = await noteContent.textContent();208    expect(text?.length).toBeGreaterThan(4000);209    await ctx.close();210  });211212  test("EF10 - notes field shows correct label", async () => {213    const { popup } = await setupVault(ctx);214    await popup.locator('input[placeholder="search vault..."]').click();215    await popup.getByRole("button", { name: /new entry/i }).click();216    await popup.locator(".peach-new-name-input").fill("NotesLabel");217    await popup.locator('input[placeholder="domain.com"]').fill("https://label.com");218    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");219    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");220    await popup.locator("textarea").fill("test notes");221    await popup.getByRole("button", { name: /create/i }).click();222    await popup.waitForTimeout(1500);223    await popup.locator(".peach-entry-name-button").first().click();224    await popup.waitForTimeout(500);225    const label = popup.locator(".peach-label").filter({ hasText: "notes" }).first();226    await expect(label).toBeVisible({ timeout: 3000 });227    await ctx.close();228  });229});230231test.describe("URL Field", () => {232  let ctx: BrowserContext;233  test.afterEach(async () => { await ctx.close(); });234  test.beforeEach(async () => { ctx = await makeContext(); });235236  test("EF11 - URL field accepts https://", async () => {237    const { popup } = await setupVault(ctx);238    await popup.locator('input[placeholder="search vault..."]').click();239    await popup.getByRole("button", { name: /new entry/i }).click();240    await popup.locator(".peach-new-name-input").fill("HTTPSURL");241    await popup.locator('input[placeholder="domain.com"]').fill("https://https.com");242    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");243    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");244    await popup.getByRole("button", { name: /create/i }).click();245    await popup.waitForTimeout(1500);246    await expect(popup.locator("text=HTTPSURL")).toBeVisible({ timeout: 3000 });247    await ctx.close();248  });249250  test("EF12 - URL field accepts http://", async () => {251    const { popup } = await setupVault(ctx);252    await popup.locator('input[placeholder="search vault..."]').click();253    await popup.getByRole("button", { name: /new entry/i }).click();254    await popup.locator(".peach-new-name-input").fill("HTTPURL");255    await popup.locator('input[placeholder="domain.com"]').fill("http://http.com");256    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");257    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");258    await popup.getByRole("button", { name: /create/i }).click();259    await popup.waitForTimeout(1500);260    await expect(popup.locator("text=HTTPURL")).toBeVisible({ timeout: 3000 });261    await ctx.close();262  });263264  test("EF13 - URL field accepts ftp://", async () => {265    const { popup } = await setupVault(ctx);266    await popup.locator('input[placeholder="search vault..."]').click();267    await popup.getByRole("button", { name: /new entry/i }).click();268    await popup.locator(".peach-new-name-input").fill("FTPURL");269    await popup.locator('input[placeholder="domain.com"]').fill("ftp://ftp.com");270    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");271    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");272    await popup.getByRole("button", { name: /create/i }).click();273    await popup.waitForTimeout(1500);274    await expect(popup.locator("text=FTPURL")).toBeVisible({ timeout: 3000 });275    await ctx.close();276  });277278  test("EF14 - URL with path is saved correctly", async () => {279    const { popup } = await setupVault(ctx);280    await popup.locator('input[placeholder="search vault..."]').click();281    await popup.getByRole("button", { name: /new entry/i }).click();282    await popup.locator(".peach-new-name-input").fill("PathURL");283    await popup.locator('input[placeholder="domain.com"]').fill("https://example.com/path/to/page?query=value#anchor");284    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");285    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");286    await popup.getByRole("button", { name: /create/i }).click();287    await popup.waitForTimeout(1500);288    await expect(popup.locator("text=PathURL")).toBeVisible({ timeout: 3000 });289    await ctx.close();290  });291292  test("EF15 - URL with port is saved correctly", async () => {293    const { popup } = await setupVault(ctx);294    await popup.locator('input[placeholder="search vault..."]').click();295    await popup.getByRole("button", { name: /new entry/i }).click();296    await popup.locator(".peach-new-name-input").fill("PortURL");297    await popup.locator('input[placeholder="domain.com"]').fill("https://example.com:8443/login");298    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");299    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");300    await popup.getByRole("button", { name: /create/i }).click();301    await popup.waitForTimeout(1500);302    await expect(popup.locator("text=PortURL")).toBeVisible({ timeout: 3000 });303    await ctx.close();304  });305});306307test.describe("Username Field", () => {308  let ctx: BrowserContext;309  test.afterEach(async () => { await ctx.close(); });310  test.beforeEach(async () => { ctx = await makeContext(); });311312  test("EF16 - email format username", async () => {313    const { popup } = await setupVault(ctx);314    await popup.locator('input[placeholder="search vault..."]').click();315    await popup.getByRole("button", { name: /new entry/i }).click();316    await popup.locator(".peach-new-name-input").fill("EmailUser");317    await popup.locator('input[placeholder="domain.com"]').fill("https://email.com");318    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");319    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");320    await popup.getByRole("button", { name: /create/i }).click();321    await popup.waitForTimeout(1500);322    await expect(popup.locator("text=EmailUser")).toBeVisible({ timeout: 3000 });323    await ctx.close();324  });325326  test("EF17 - username with special characters", async () => {327    const { popup } = await setupVault(ctx);328    await popup.locator('input[placeholder="search vault..."]').click();329    await popup.getByRole("button", { name: /new entry/i }).click();330    await popup.locator(".peach-new-name-input").fill("SpecialUser");331    await popup.locator('input[placeholder="domain.com"]').fill("https://special.com");332    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");333    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");334    await popup.getByRole("button", { name: /create/i }).click();335    await popup.waitForTimeout(1500);336    await expect(popup.locator("text=SpecialUser")).toBeVisible({ timeout: 3000 });337    await ctx.close();338  });339340  test("EF18 - very long username", async () => {341    const { popup } = await setupVault(ctx);342    await popup.locator('input[placeholder="search vault..."]').click();343    await popup.getByRole("button", { name: /new entry/i }).click();344    await popup.locator(".peach-new-name-input").fill("LongUser");345    await popup.locator('input[placeholder="domain.com"]').fill("https://long.com");346    await popup.locator('input[placeholder="username or email"]').fill("a".repeat(200) + "@test.com");347    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");348    await popup.getByRole("button", { name: /create/i }).click();349    await popup.waitForTimeout(1500);350    await expect(popup.locator("text=LongUser")).toBeVisible({ timeout: 3000 });351    await ctx.close();352  });353354  test("EF19 - username with unicode", async () => {355    const { popup } = await setupVault(ctx);356    await popup.locator('input[placeholder="search vault..."]').click();357    await popup.getByRole("button", { name: /new entry/i }).click();358    await popup.locator(".peach-new-name-input").fill("UnicodeUser");359    await popup.locator('input[placeholder="domain.com"]').fill("https://unicode.com");360    await popup.locator('input[placeholder="username or email"]').fill("用户@例子.测试");361    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");362    await popup.getByRole("button", { name: /create/i }).click();363    await popup.waitForTimeout(1500);364    await expect(popup.locator("text=UnicodeUser")).toBeVisible({ timeout: 3000 });365    await ctx.close();366  });367});368369test.describe("Password Field", () => {370  let ctx: BrowserContext;371  test.afterEach(async () => { await ctx.close(); });372  test.beforeEach(async () => { ctx = await makeContext(); });373374  test("EF20 - emoji password", async () => {375    const { popup } = await setupVault(ctx);376    await popup.locator('input[placeholder="search vault..."]').click();377    await popup.getByRole("button", { name: /new entry/i }).click();378    await popup.locator(".peach-new-name-input").fill("EmojiPass");379    await popup.locator('input[placeholder="domain.com"]').fill("https://emoji.com");380    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");381    await popup.locator(".peach-new-field-input[placeholder='password']").fill("🔐🔑🌟💻🖥️⌨️");382    await popup.getByRole("button", { name: /create/i }).click();383    await popup.waitForTimeout(1500);384    await expect(popup.locator("text=EmojiPass")).toBeVisible({ timeout: 3000 });385    await ctx.close();386  });387388  test("EF21 - whitespace-only password", async () => {389    const { popup } = await setupVault(ctx);390    await popup.locator('input[placeholder="search vault..."]').click();391    await popup.getByRole("button", { name: /new entry/i }).click();392    await popup.locator(".peach-new-name-input").fill("SpacePass");393    await popup.locator('input[placeholder="domain.com"]').fill("https://space.com");394    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");395    await popup.locator(".peach-new-field-input[placeholder='password']").fill("     ");396    await popup.getByRole("button", { name: /create/i }).click();397    await popup.waitForTimeout(1500);398    await expect(popup.locator("text=SpacePass")).toBeVisible({ timeout: 3000 });399    await ctx.close();400  });401402  test("EF22 - very long password", async () => {403    const { popup } = await setupVault(ctx);404    await popup.locator('input[placeholder="search vault..."]').click();405    await popup.getByRole("button", { name: /new entry/i }).click();406    await popup.locator(".peach-new-name-input").fill("LongPass");407    await popup.locator('input[placeholder="domain.com"]').fill("https://long.com");408    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");409    await popup.locator(".peach-new-field-input[placeholder='password']").fill("a".repeat(1000));410    await popup.getByRole("button", { name: /create/i }).click();411    await popup.waitForTimeout(1500);412    await expect(popup.locator("text=LongPass")).toBeVisible({ timeout: 3000 });413    await ctx.close();414  });415416  test("EF23 - password reveal button works", async () => {417    const { popup } = await setupVault(ctx);418    await popup.locator('input[placeholder="search vault..."]').click();419    await popup.getByRole("button", { name: /new entry/i }).click();420    await popup.locator(".peach-new-name-input").fill("RevealPass");421    await popup.locator('input[placeholder="domain.com"]').fill("https://reveal.com");422    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");423    await popup.locator(".peach-new-field-input[placeholder='password']").fill("secretpass");424    await popup.getByRole("button", { name: /create/i }).click();425    await popup.waitForTimeout(1500);426    await popup.locator(".peach-entry-name-button").first().click();427    await popup.waitForTimeout(500);428    const eyeBtn = popup.locator("button[aria-label='Reveal password']");429    if (await eyeBtn.isVisible({ timeout: 2000 }).catch(() => false)) {430      await eyeBtn.click();431      await popup.waitForTimeout(300);432    }433    await ctx.close();434  });435});436437test.describe("Entry Editing", () => {438  let ctx: BrowserContext;439  test.afterEach(async () => { await ctx.close(); });440  test.beforeEach(async () => { ctx = await makeContext(); });441442  test("EF24 - edit entry changes name", async () => {443    const { popup } = await setupVault(ctx);444    await popup.locator('input[placeholder="search vault..."]').click();445    await popup.getByRole("button", { name: /new entry/i }).click();446    await popup.locator(".peach-new-name-input").fill("OriginalName");447    await popup.locator('input[placeholder="domain.com"]').fill("https://original.com");448    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");449    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");450    await popup.getByRole("button", { name: /create/i }).click();451    await popup.waitForTimeout(1500);452    await popup.locator(".peach-entry-name-button").first().click();453    await popup.waitForTimeout(500);454    await popup.locator("button[aria-label='Edit']").first().click();455    await popup.waitForTimeout(500);456    await popup.locator(".peach-new-name-input").fill("ChangedName");457    await popup.getByRole("button", { name: /save/i }).click();458    await popup.waitForTimeout(1500);459    await expect(popup.locator("text=ChangedName")).toBeVisible({ timeout: 3000 });460    await ctx.close();461  });462463  test("EF25 - edit entry preserves other fields", async () => {464    const { popup } = await setupVault(ctx);465    await popup.locator('input[placeholder="search vault..."]').click();466    await popup.getByRole("button", { name: /new entry/i }).click();467    await popup.locator(".peach-new-name-input").fill("PreserveFields");468    await popup.locator('input[placeholder="domain.com"]').fill("https://preserve.com");469    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");470    await popup.locator(".peach-new-field-input[placeholder='password']").fill("originalpass");471    await popup.getByRole("button", { name: /create/i }).click();472    await popup.waitForTimeout(1500);473    await popup.locator(".peach-entry-name-button").first().click();474    await popup.waitForTimeout(500);475    await popup.locator("button[aria-label='Edit']").first().click();476    await popup.waitForTimeout(500);477    await popup.locator(".peach-new-name-input").fill("PreserveFieldsChanged");478    await popup.getByRole("button", { name: /save/i }).click();479    await popup.waitForTimeout(1500);480    await popup.locator(".peach-entry-name-button").first().click();481    await popup.waitForTimeout(500);482    const username = popup.locator(".peach-field-value").filter({ hasText: "[email protected]" }).first();483    await expect(username).toBeVisible({ timeout: 3000 });484    await ctx.close();485  });486487  test("EF26 - cancel edit reverts changes", async () => {488    const { popup } = await setupVault(ctx);489    await popup.locator('input[placeholder="search vault..."]').click();490    await popup.getByRole("button", { name: /new entry/i }).click();491    await popup.locator(".peach-new-name-input").fill("CancelEdit");492    await popup.locator('input[placeholder="domain.com"]').fill("https://cancel.com");493    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");494    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");495    await popup.getByRole("button", { name: /create/i }).click();496    await popup.waitForTimeout(1500);497    await popup.locator(".peach-entry-name-button").first().click();498    await popup.waitForTimeout(500);499    await popup.locator("button[aria-label='Edit']").first().click();500    await popup.waitForTimeout(500);501    await popup.locator(".peach-new-name-input").fill("ShouldNotChange");502    await popup.keyboard.press("Escape");503    await popup.waitForTimeout(300);504    await expect(popup.locator("text=CancelEdit")).toBeVisible({ timeout: 3000 });505    await ctx.close();506  });507508  test("EF27 - delete entry from detail view", async () => {509    const { popup } = await setupVault(ctx);510    await popup.locator('input[placeholder="search vault..."]').click();511    await popup.getByRole("button", { name: /new entry/i }).click();512    await popup.locator(".peach-new-name-input").fill("DeleteMeEntry");513    await popup.locator('input[placeholder="domain.com"]').fill("https://delete.com");514    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");515    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");516    await popup.getByRole("button", { name: /create/i }).click();517    await popup.waitForTimeout(1500);518    await popup.locator(".peach-entry-name-button").first().click();519    await popup.waitForTimeout(500);520    const deleteLink = popup.locator("text=delete entry");521    await deleteLink.click();522    await popup.waitForTimeout(1000);523    await expect(popup.locator("text=DeleteMeEntry")).not.toBeVisible({ timeout: 3000 });524    await ctx.close();525  });526});527528test.describe("Entry State Transitions", () => {529  let ctx: BrowserContext;530  test.afterEach(async () => { await ctx.close(); });531  test.beforeEach(async () => { ctx = await makeContext(); });532533  test("EF28 - list to detail transition", async () => {534    const { popup } = await setupVault(ctx);535    await popup.locator('input[placeholder="search vault..."]').click();536    await popup.getByRole("button", { name: /new entry/i }).click();537    await popup.locator(".peach-new-name-input").fill("ListDetail");538    await popup.locator('input[placeholder="domain.com"]').fill("https://listdetail.com");539    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");540    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");541    await popup.getByRole("button", { name: /create/i }).click();542    await popup.waitForTimeout(1500);543    await popup.locator(".peach-entry-name-button").first().click();544    await popup.waitForTimeout(500);545    const usernameField = popup.locator(".peach-field-value").filter({ hasText: "[email protected]" }).first();546    await expect(usernameField).toBeVisible({ timeout: 3000 });547    await ctx.close();548  });549550  test("EF29 - detail to list back transition", async () => {551    const { popup } = await setupVault(ctx);552    await popup.locator('input[placeholder="search vault..."]').click();553    await popup.getByRole("button", { name: /new entry/i }).click();554    await popup.locator(".peach-new-name-input").fill("DetailBack");555    await popup.locator('input[placeholder="domain.com"]').fill("https://back.com");556    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");557    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");558    await popup.getByRole("button", { name: /create/i }).click();559    await popup.waitForTimeout(1500);560    await popup.locator(".peach-entry-name-button").first().click();561    await popup.waitForTimeout(500);562    await popup.locator("button[aria-label='Back']").first().click();563    await popup.waitForTimeout(300);564    await expect(popup.locator("text=DetailBack")).toBeVisible({ timeout: 3000 });565    await ctx.close();566  });567568  test("EF30 - entry to edit mode transition", async () => {569    const { popup } = await setupVault(ctx);570    await popup.locator('input[placeholder="search vault..."]').click();571    await popup.getByRole("button", { name: /new entry/i }).click();572    await popup.locator(".peach-new-name-input").fill("EditMode");573    await popup.locator('input[placeholder="domain.com"]').fill("https://edit.com");574    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");575    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");576    await popup.getByRole("button", { name: /create/i }).click();577    await popup.waitForTimeout(1500);578    await popup.locator(".peach-entry-name-button").first().click();579    await popup.waitForTimeout(500);580    await popup.locator("button[aria-label='Edit']").first().click();581    await popup.waitForTimeout(500);582    const nameInput = popup.locator(".peach-new-name-input");583    await expect(nameInput).toBeVisible({ timeout: 3000 });584    await ctx.close();585  });586587  test("EF31 - edit to detail transition after save", async () => {588    const { popup } = await setupVault(ctx);589    await popup.locator('input[placeholder="search vault..."]').click();590    await popup.getByRole("button", { name: /new entry/i }).click();591    await popup.locator(".peach-new-name-input").fill("EditSave");592    await popup.locator('input[placeholder="domain.com"]').fill("https://editsave.com");593    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");594    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");595    await popup.getByRole("button", { name: /create/i }).click();596    await popup.waitForTimeout(1500);597    await popup.locator(".peach-entry-name-button").first().click();598    await popup.waitForTimeout(500);599    await popup.locator("button[aria-label='Edit']").first().click();600    await popup.waitForTimeout(500);601    await popup.locator(".peach-new-name-input").fill("EditSaveChanged");602    await popup.getByRole("button", { name: /save/i }).click();603    await popup.waitForTimeout(1500);604    const usernameField = popup.locator(".peach-field-value").filter({ hasText: "[email protected]" }).first();605    await expect(usernameField).toBeVisible({ timeout: 3000 });606    await ctx.close();607  });608});609610test.describe("Entry List Behavior", () => {611  let ctx: BrowserContext;612  test.afterEach(async () => { await ctx.close(); });613  test.beforeEach(async () => { ctx = await makeContext(); });614615  test("EF32 - entries sorted alphabetically", async () => {616    const { popup } = await setupVault(ctx);617    for (const name of ["Zebra", "Apple", "Mango"]) {618      await popup.locator('input[placeholder="search vault..."]').click();619      await popup.getByRole("button", { name: /new entry/i }).click();620      await popup.locator(".peach-new-name-input").fill(name);621      await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);622      await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);623      await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");624      await popup.getByRole("button", { name: /create/i }).click();625      await popup.waitForTimeout(800);626    }627    const entries = popup.locator(".peach-entry-row-name");628    const count = await entries.count();629    expect(count).toBeGreaterThanOrEqual(3);630    await ctx.close();631  });632633  test("EF33 - breach indicator shows for compromised entry", async () => {634    const { popup } = await setupVault(ctx);635    await popup.waitForTimeout(500);636    const breachRow = popup.locator(".peach-entry-row.is-breach, .peach-entry-row-dot");637    await breachRow.isVisible({ timeout: 2000 }).catch(() => false);638    await ctx.close();639  });640641  test("EF34 - entry count updates after add", async () => {642    const { popup } = await setupVault(ctx);643    const counter = popup.locator(".peach-search-counter");644    const initial = await counter.textContent();645    await popup.locator('input[placeholder="search vault..."]').click();646    await popup.getByRole("button", { name: /new entry/i }).click();647    await popup.locator(".peach-new-name-input").fill("CountTest");648    await popup.locator('input[placeholder="domain.com"]').fill("https://count.com");649    await popup.locator('input[placeholder="username or email"]').fill("[email protected]");650    await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");651    await popup.getByRole("button", { name: /create/i }).click();652    await popup.waitForTimeout(1500);653    const final = await counter.textContent();654    expect(initial).not.toEqual(final);655    await ctx.close();656  });657658  test("EF35 - next/prev navigation works", async () => {659    const { popup } = await setupVault(ctx);660    for (const name of ["FirstEntry", "SecondEntry", "ThirdEntry"]) {661      await popup.locator('input[placeholder="search vault..."]').click();662      await popup.getByRole("button", { name: /new entry/i }).click();663      await popup.locator(".peach-new-name-input").fill(name);664      await popup.locator('input[placeholder="domain.com"]').fill(`https://${name.toLowerCase()}.com`);665      await popup.locator('input[placeholder="username or email"]').fill(`${name.toLowerCase()}@user.com`);666      await popup.locator(".peach-new-field-input[placeholder='password']").fill("pass");667      await popup.getByRole("button", { name: /create/i }).click();668      await popup.waitForTimeout(800);669    }670    const nextBtn = popup.getByRole("button", { name: /next/i });671    if (await nextBtn.isVisible({ timeout: 2000 }).catch(() => false)) {672      await nextBtn.click();673      await popup.waitForTimeout(300);674      const prevBtn = popup.getByRole("button", { name: /prev/i });675      if (await prevBtn.isVisible({ timeout: 1000 }).catch(() => false)) {676        await prevBtn.click();677        await popup.waitForTimeout(300);678      }679    }680    await ctx.close();681  });682});