import { test, expect, Page, BrowserContext } from "@playwright/test"; import { UNLOCKED, VAULT_PASSWORD, addEntry, makeContext, setupVault } from "./helpers/vault"; // 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(); const unlockInput = popup.locator("#unlock-password"); if (await unlockInput.isVisible({ timeout: 2000 }).catch(() => false)) { await unlockInput.fill(VAULT_PASSWORD); await popup.getByRole("button", { name: /unlock/i }).click(); } await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 20000 }); } 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); const searchInput = popup.locator('input[placeholder="search vault..."]'); await expect(searchInput).toBeVisible(); await ctx.close(); }); test("S02 - search input has placeholder text", async () => { const { popup } = await setupVault(ctx); const searchInput = popup.locator('input[placeholder="search vault..."]'); const placeholder = await searchInput.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 popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').fill("nonexistent"); await popup.waitForTimeout(500); const noResults = popup.locator("text=/no results|not found|0 results/i"); const hasNoResults = await noResults.isVisible({ timeout: 2000 }).catch(() => false); expect(hasNoResults).toBeTruthy(); 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 popup.locator('input[placeholder="search vault..."]').fill("EntryOne"); await popup.waitForTimeout(300); await popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').fill("Entry5"); await popup.waitForTimeout(300); await popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').fill("."); await popup.waitForTimeout(300); await ctx.close(); }); test("S12 - search input has proper aria-label", async () => { const { popup } = await setupVault(ctx); const searchInput = popup.locator('input[placeholder="search vault..."]'); const ariaLabel = await searchInput.getAttribute("aria-label"); expect(ariaLabel).toBeTruthy(); await ctx.close(); }); test("S13 - search clears on escape", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "TestEntry"); await popup.locator('input[placeholder="search vault..."]').fill("Test"); await popup.waitForTimeout(300); await popup.keyboard.press("Escape"); await popup.waitForTimeout(200); const value = await popup.locator('input[placeholder="search vault..."]').inputValue(); expect(value).toBe(""); await ctx.close(); }); test("S14 - search debounce works", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "TestEntry"); const searchInput = popup.locator('input[placeholder="search vault..."]'); 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 popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').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("SA02 - search remembers previous query", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Test1"); await popup.locator('input[placeholder="search vault..."]').fill("Test1"); await popup.waitForTimeout(300); await popup.locator(".peach-entry-name-button").first().click(); await popup.waitForTimeout(300); await popup.keyboard.press("Escape"); await popup.waitForTimeout(200); const value = await popup.locator('input[placeholder="search vault..."]').inputValue(); expect(value).toBe("Test1"); await ctx.close(); }); test("SA03 - search field does not accept invalid characters", async () => { const { popup } = await setupVault(ctx); await createEntry(popup, "Test"); const searchInput = popup.locator('input[placeholder="search vault..."]'); 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 popup.locator('input[placeholder="search vault..."]').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 popup.locator('input[placeholder="search vault..."]').fill("Тест"); await popup.waitForTimeout(300); await ctx.close(); }); });