import { test, expect, Page, BrowserContext } from "@playwright/test"; import { UNLOCKED, VAULT_PASSWORD, addEntry, listSearch, makeContext, openList, setupVault } from "./helpers/vault"; // The top bar's search field is readOnly — it only opens the list overlay, // where the editable field lives. Specs that filled the top field were // filling a readOnly input. const topSearch = (popup: Page) => popup.locator(".peach-search-wrap .peach-search-input"); // Real vault creation runs Argon2id; seeded entries need a popup reload each. test.describe.configure({ timeout: 90_000 }); // Entries are added through the background, which the open popup does not // observe — reload so the list re-reads the vault. async function createEntry(popup: Page, name: string, domain = "test.com") { await addEntry(popup, { title: name, url: `https://${domain}`, username: `${name.toLowerCase()}@${domain}`, password: "Passphrase123!", }); await popup.reload(); // The reload lands on either the lock screen or the vault; waiting for one // of them first avoids racing the render. await popup.waitForSelector(`#unlock-password, ${UNLOCKED}`, { timeout: 30000 }); const unlockInput = popup.locator("#unlock-password"); if (await unlockInput.isVisible().catch(() => false)) { await unlockInput.fill(VAULT_PASSWORD); await popup.getByRole("button", { name: /unlock/i }).click(); } await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 30000 }); await openList(popup); } test.describe("Search", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("S01 - search input is visible", async () => { const { popup } = await setupVault(ctx); await expect(topSearch(popup)).toBeVisible(); await ctx.close(); }); test("S02 - search input has placeholder text", async () => { const { popup } = await setupVault(ctx); const placeholder = await topSearch(popup).getAttribute("placeholder"); expect(placeholder).toContain("search"); await ctx.close(); }); test("S03 - search filters entries by name", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "GoogleEntry"); await createEntry(popup, "AmazonEntry"); await listSearch(popup).fill("Google"); await popup.waitForTimeout(300); const entries = popup.locator(".peach-entry-row"); const count = await entries.count(); expect(count).toBe(1); await ctx.close(); }); test("S04 - search is case-insensitive", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "TestEntry"); await listSearch(popup).fill("test"); await popup.waitForTimeout(300); const entries = popup.locator(".peach-entry-row"); const count = await entries.count(); expect(count).toBe(1); await ctx.close(); }); test("S05 - search with no results shows empty state", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Entry1"); await listSearch(popup).fill("nonexistent"); // The list's empty state is .peach-list-empty ("no entries yet"); there is // no separate "no results" copy for a filtered-to-nothing search. await expect(popup.locator(".peach-list-empty")).toBeVisible({ timeout: 5000 }); await expect(popup.locator(".peach-entry-row")).toHaveCount(0); await ctx.close(); }); test("S06 - clearing search shows all entries", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "EntryOne"); await createEntry(popup, "EntryTwo"); await listSearch(popup).fill("EntryOne"); await popup.waitForTimeout(300); await listSearch(popup).clear(); await popup.waitForTimeout(300); const entries = popup.locator(".peach-entry-row"); const count = await entries.count(); expect(count).toBe(2); await ctx.close(); }); test("S07 - search by domain works", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Site1", "google.com"); await createEntry(popup, "Site2", "amazon.com"); await listSearch(popup).fill("google"); await popup.waitForTimeout(300); const entries = popup.locator(".peach-entry-row"); const count = await entries.count(); expect(count).toBe(1); await ctx.close(); }); test("S08 - search by username works", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "User1"); await createEntry(popup, "User2"); await listSearch(popup).fill("user1@test.com"); await popup.waitForTimeout(300); const entries = popup.locator(".peach-entry-row"); const count = await entries.count(); expect(count).toBe(1); await ctx.close(); }); test("S09 - search preserves scroll position", async () => { const { popup } = await setupVault(ctx); for (let i = 0; i < 15; i++) { await createEntry(popup, `Entry${i}`, `site${i}.com`); } await listSearch(popup).fill("Entry5"); await popup.waitForTimeout(300); await listSearch(popup).clear(); await popup.waitForTimeout(300); await ctx.close(); }); test("S10 - search with special characters", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Test@Entry"); await listSearch(popup).fill("@"); await popup.waitForTimeout(300); await ctx.close(); }); test("S11 - search with regex characters", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Test.Entry"); await listSearch(popup).fill("."); await popup.waitForTimeout(300); await ctx.close(); }); test("S12 - search input has proper aria-label", async () => { const { popup } = await setupVault(ctx); const ariaLabel = await topSearch(popup).getAttribute("aria-label"); expect(ariaLabel).toBeTruthy(); await ctx.close(); }); test("S13 - escape closes the list overlay", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "TestEntry"); await listSearch(popup).fill("Test"); await popup.keyboard.press("Escape"); // Escape dismisses the list rather than emptying the field. await expect(listSearch(popup)).toHaveCount(0, { timeout: 5000 }); await ctx.close(); }); test("S14 - search debounce works", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "TestEntry"); const searchInput = listSearch(popup); await searchInput.fill("T"); await popup.waitForTimeout(100); await searchInput.fill("Te"); await popup.waitForTimeout(100); await searchInput.fill("Tes"); await popup.waitForTimeout(300); await ctx.close(); }); test("S15 - search highlights matching text", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "TestingEntry"); await listSearch(popup).fill("Test"); await popup.waitForTimeout(300); const highlighted = popup.locator("[class*='highlight' i], [class*='match' i]").first(); const hasHighlight = await highlighted.isVisible().catch(() => false); if (hasHighlight) expect(true).toBeTruthy(); await ctx.close(); }); test("S16 - empty search shows all entries", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Entry1"); await createEntry(popup, "Entry2"); await listSearch(popup).fill(""); await popup.waitForTimeout(300); const entries = popup.locator(".peach-entry-row"); const count = await entries.count(); expect(count).toBe(2); await ctx.close(); }); test("S17 - search accessible via keyboard", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "TestEntry"); await popup.keyboard.press("/"); await popup.waitForTimeout(200); const overlaySearch = popup.locator(".peach-overlay-view .peach-search-input"); await expect(overlaySearch).toBeVisible(); const focused = await overlaySearch.evaluate((el) => document.activeElement === el); expect(focused).toBeTruthy(); await ctx.close(); }); test("S18 - partial match works", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "MyPasswordEntry"); await listSearch(popup).fill("Pass"); await popup.waitForTimeout(300); const entries = popup.locator(".peach-entry-row"); const count = await entries.count(); expect(count).toBe(1); await ctx.close(); }); }); test.describe("Search - Advanced", () => { let ctx: BrowserContext; test.afterEach(async () => { await ctx.close(); }); test.beforeEach(async () => { ctx = await makeContext(); }); test("SA01 - search with spaces works", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "My Test Entry"); await listSearch(popup).fill("Test"); await popup.waitForTimeout(300); const entries = popup.locator(".peach-entry-row"); const count = await entries.count(); expect(count).toBe(1); await ctx.close(); }); // The list search does NOT persist across close/reopen — reopening the // list starts from an empty query. Asserted as observed. test("SA02 - reopening the list starts from an empty query", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Test1"); await listSearch(popup).fill("Test1"); await popup.locator(".peach-entry-row").first().click(); await expect(listSearch(popup)).toHaveCount(0, { timeout: 5000 }); await openList(popup); await expect(listSearch(popup)).toHaveValue(""); await ctx.close(); }); test("SA03 - search field does not accept invalid characters", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Test"); const searchInput = listSearch(popup); await searchInput.fill("Test\r\n"); await ctx.close(); }); test("SA04 - search with very long query", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Short"); const longQuery = "a".repeat(100); await listSearch(popup).fill(longQuery); await popup.waitForTimeout(300); await ctx.close(); }); test("SA05 - search in non-latin characters", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Тест"); await listSearch(popup).fill("Тест"); await popup.waitForTimeout(300); await ctx.close(); }); });