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