import { test, expect, Page, BrowserContext } from "@playwright/test"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import os from "os"; import fs from "fs/promises"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const EXTENSION_PATH = join(__dirname, "..", "dist"); const VAULT_PASSWORD = "CorrectHorseBattery42!"; async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise { const start = Date.now(); let sw = ctx.serviceWorkers()[0]; while (!sw && Date.now() - start < maxWait) { await new Promise(r => setTimeout(r, 500)); sw = ctx.serviceWorkers()[0]; } if (!sw) throw new Error(`No SW after ${maxWait}ms`); return new URL(sw.url()).host; } async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page, extId: string }> { const extId = await waitForSW(ctx); const popup = await ctx.newPage(); await popup.goto(`chrome-extension://${extId}/src/popup/index.html`); await popup.waitForTimeout(3000); return { popup, extId }; } async function makeContext() { const { chromium } = await import("playwright"); const userDataDir = join(os.tmpdir(), `peach-${Date.now()}-${Math.random().toString(36).slice(2)}`); await fs.mkdir(userDataDir, { recursive: true }); return chromium.launchPersistentContext(userDataDir, { headless: false, args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`], }); } async function setupVault(ctx: BrowserContext, password = VAULT_PASSWORD) { const { popup } = await loadPopup(ctx); const pw = popup.locator("#master-password"); if (await pw.isVisible()) { await pw.fill(password); await popup.locator("#confirm-password").fill(password); await popup.waitForFunction(() => { const submit = document.querySelector(".peach-lock-submit"); return submit instanceof HTMLButtonElement && !submit.disabled; }); await popup.getByRole("button", { name: /create vault/i }).click(); await popup.waitForTimeout(2000); } return { popup }; } async function createEntry(popup: Page, name: string, domain = "test.com") { await popup.evaluate( async ({ entryName, entryDomain }) => { await new Promise((resolve) => { chrome.runtime.sendMessage( { type: "ADD_ENTRY", payload: { type: "login", title: entryName, url: `https://${entryDomain}`, username: `${entryName.toLowerCase()}@${entryDomain}`, password: "Passphrase123!", }, }, resolve ); }); }, { entryName: name, entryDomain: domain } ); await popup.reload(); await popup.waitForTimeout(1500); 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 popup.waitForTimeout(1500); } } 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(); }); });