1import { test, expect, Page, 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");10const VAULT_PASSWORD = "CorrectHorseBattery42!";1112async function waitForSW(ctx: BrowserContext, maxWait = 15000): Promise<string> {13 const start = Date.now();14 let sw = ctx.serviceWorkers()[0];15 while (!sw && Date.now() - start < maxWait) {16 await new Promise(r => setTimeout(r, 500));17 sw = ctx.serviceWorkers()[0];18 }19 if (!sw) throw new Error(`No SW after ${maxWait}ms`);20 return new URL(sw.url()).host;21}2223async function loadPopup(ctx: BrowserContext): Promise<{ popup: Page, extId: string }> {24 const extId = await waitForSW(ctx);25 const popup = await ctx.newPage();26 await popup.goto(`chrome-extension://${extId}/src/popup/index.html`);27 await popup.waitForTimeout(3000);28 return { popup, extId };29}3031async function makeContext() {32 const { chromium } = await import("playwright");33 const userDataDir = join(os.tmpdir(), `peach-${Date.now()}-${Math.random().toString(36).slice(2)}`);34 await fs.mkdir(userDataDir, { recursive: true });35 return chromium.launchPersistentContext(userDataDir, {36 headless: false,37 args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],38 });39}4041async function setupVault(ctx: BrowserContext, password = VAULT_PASSWORD) {42 const { popup } = await loadPopup(ctx);43 const pw = popup.locator("#master-password");44 if (await pw.isVisible()) {45 await pw.fill(password);46 await popup.locator("#confirm-password").fill(password);47 await popup.waitForFunction(() => {48 const submit = document.querySelector(".peach-lock-submit");49 return submit instanceof HTMLButtonElement && !submit.disabled;50 });51 await popup.getByRole("button", { name: /create vault/i }).click();52 await popup.waitForTimeout(2000);53 }54 return { popup };55}5657async function createEntry(popup: Page, name: string, domain = "test.com") {58 await popup.evaluate(59 async ({ entryName, entryDomain }) => {60 await new Promise((resolve) => {61 chrome.runtime.sendMessage(62 {63 type: "ADD_ENTRY",64 payload: {65 type: "login",66 title: entryName,67 url: `https://${entryDomain}`,68 username: `${entryName.toLowerCase()}@${entryDomain}`,69 password: "Passphrase123!",70 },71 },72 resolve73 );74 });75 },76 { entryName: name, entryDomain: domain }77 );7879 await popup.reload();80 await popup.waitForTimeout(1500);8182 const unlockInput = popup.locator("#unlock-password");83 if (await unlockInput.isVisible().catch(() => false)) {84 await unlockInput.fill(VAULT_PASSWORD);85 await popup.getByRole("button", { name: /unlock/i }).click();86 await popup.waitForTimeout(1500);87 }88}8990test.describe("Search", () => {91 let ctx: BrowserContext;92 test.afterEach(async () => { await ctx.close(); });93 test.beforeEach(async () => { ctx = await makeContext(); });9495 test("S01 - search input is visible", async () => {96 const { popup } = await setupVault(ctx);97 const searchInput = popup.locator('input[placeholder="search vault..."]');98 await expect(searchInput).toBeVisible();99 await ctx.close();100 });101102 test("S02 - search input has placeholder text", async () => {103 const { popup } = await setupVault(ctx);104 const searchInput = popup.locator('input[placeholder="search vault..."]');105 const placeholder = await searchInput.getAttribute("placeholder");106 expect(placeholder).toContain("search");107 await ctx.close();108 });109110 test("S03 - search filters entries by name", async () => {111 const { popup } = await setupVault(ctx);112 await createEntry(popup, "GoogleEntry");113 await createEntry(popup, "AmazonEntry");114 await popup.locator('input[placeholder="search vault..."]').fill("Google");115 await popup.waitForTimeout(300);116 const entries = popup.locator(".peach-entry-row");117 const count = await entries.count();118 expect(count).toBe(1);119 await ctx.close();120 });121122 test("S04 - search is case-insensitive", async () => {123 const { popup } = await setupVault(ctx);124 await createEntry(popup, "TestEntry");125 await popup.locator('input[placeholder="search vault..."]').fill("test");126 await popup.waitForTimeout(300);127 const entries = popup.locator(".peach-entry-row");128 const count = await entries.count();129 expect(count).toBe(1);130 await ctx.close();131 });132133 test("S05 - search with no results shows empty state", async () => {134 const { popup } = await setupVault(ctx);135 await createEntry(popup, "Entry1");136 await popup.locator('input[placeholder="search vault..."]').fill("nonexistent");137 await popup.waitForTimeout(500);138 const noResults = popup.locator("text=/no results|not found|0 results/i");139 const hasNoResults = await noResults.isVisible({ timeout: 2000 }).catch(() => false);140 expect(hasNoResults).toBeTruthy();141 await ctx.close();142 });143144 test("S06 - clearing search shows all entries", async () => {145 const { popup } = await setupVault(ctx);146 await createEntry(popup, "EntryOne");147 await createEntry(popup, "EntryTwo");148 await popup.locator('input[placeholder="search vault..."]').fill("EntryOne");149 await popup.waitForTimeout(300);150 await popup.locator('input[placeholder="search vault..."]').clear();151 await popup.waitForTimeout(300);152 const entries = popup.locator(".peach-entry-row");153 const count = await entries.count();154 expect(count).toBe(2);155 await ctx.close();156 });157158 test("S07 - search by domain works", async () => {159 const { popup } = await setupVault(ctx);160 await createEntry(popup, "Site1", "google.com");161 await createEntry(popup, "Site2", "amazon.com");162 await popup.locator('input[placeholder="search vault..."]').fill("google");163 await popup.waitForTimeout(300);164 const entries = popup.locator(".peach-entry-row");165 const count = await entries.count();166 expect(count).toBe(1);167 await ctx.close();168 });169170 test("S08 - search by username works", async () => {171 const { popup } = await setupVault(ctx);172 await createEntry(popup, "User1");173 await createEntry(popup, "User2");174 await popup.locator('input[placeholder="search vault..."]').fill("[email protected]");175 await popup.waitForTimeout(300);176 const entries = popup.locator(".peach-entry-row");177 const count = await entries.count();178 expect(count).toBe(1);179 await ctx.close();180 });181182 test("S09 - search preserves scroll position", async () => {183 const { popup } = await setupVault(ctx);184 for (let i = 0; i < 15; i++) {185 await createEntry(popup, `Entry${i}`, `site${i}.com`);186 }187 await popup.locator('input[placeholder="search vault..."]').fill("Entry5");188 await popup.waitForTimeout(300);189 await popup.locator('input[placeholder="search vault..."]').clear();190 await popup.waitForTimeout(300);191 await ctx.close();192 });193194 test("S10 - search with special characters", async () => {195 const { popup } = await setupVault(ctx);196 await createEntry(popup, "Test@Entry");197 await popup.locator('input[placeholder="search vault..."]').fill("@");198 await popup.waitForTimeout(300);199 await ctx.close();200 });201202 test("S11 - search with regex characters", async () => {203 const { popup } = await setupVault(ctx);204 await createEntry(popup, "Test.Entry");205 await popup.locator('input[placeholder="search vault..."]').fill(".");206 await popup.waitForTimeout(300);207 await ctx.close();208 });209210 test("S12 - search input has proper aria-label", async () => {211 const { popup } = await setupVault(ctx);212 const searchInput = popup.locator('input[placeholder="search vault..."]');213 const ariaLabel = await searchInput.getAttribute("aria-label");214 expect(ariaLabel).toBeTruthy();215 await ctx.close();216 });217218 test("S13 - search clears on escape", async () => {219 const { popup } = await setupVault(ctx);220 await createEntry(popup, "TestEntry");221 await popup.locator('input[placeholder="search vault..."]').fill("Test");222 await popup.waitForTimeout(300);223 await popup.keyboard.press("Escape");224 await popup.waitForTimeout(200);225 const value = await popup.locator('input[placeholder="search vault..."]').inputValue();226 expect(value).toBe("");227 await ctx.close();228 });229230 test("S14 - search debounce works", async () => {231 const { popup } = await setupVault(ctx);232 await createEntry(popup, "TestEntry");233 const searchInput = popup.locator('input[placeholder="search vault..."]');234 await searchInput.fill("T");235 await popup.waitForTimeout(100);236 await searchInput.fill("Te");237 await popup.waitForTimeout(100);238 await searchInput.fill("Tes");239 await popup.waitForTimeout(300);240 await ctx.close();241 });242243 test("S15 - search highlights matching text", async () => {244 const { popup } = await setupVault(ctx);245 await createEntry(popup, "TestingEntry");246 await popup.locator('input[placeholder="search vault..."]').fill("Test");247 await popup.waitForTimeout(300);248 const highlighted = popup.locator("[class*='highlight' i], [class*='match' i]").first();249 const hasHighlight = await highlighted.isVisible().catch(() => false);250 if (hasHighlight) expect(true).toBeTruthy();251 await ctx.close();252 });253254 test("S16 - empty search shows all entries", async () => {255 const { popup } = await setupVault(ctx);256 await createEntry(popup, "Entry1");257 await createEntry(popup, "Entry2");258 await popup.locator('input[placeholder="search vault..."]').fill("");259 await popup.waitForTimeout(300);260 const entries = popup.locator(".peach-entry-row");261 const count = await entries.count();262 expect(count).toBe(2);263 await ctx.close();264 });265266 test("S17 - search accessible via keyboard", async () => {267 const { popup } = await setupVault(ctx);268 await createEntry(popup, "TestEntry");269 await popup.keyboard.press("/");270 await popup.waitForTimeout(200);271 const overlaySearch = popup.locator(".peach-overlay-view .peach-search-input");272 await expect(overlaySearch).toBeVisible();273 const focused = await overlaySearch.evaluate((el) => document.activeElement === el);274 expect(focused).toBeTruthy();275 await ctx.close();276 });277278 test("S18 - partial match works", async () => {279 const { popup } = await setupVault(ctx);280 await createEntry(popup, "MyPasswordEntry");281 await popup.locator('input[placeholder="search vault..."]').fill("Pass");282 await popup.waitForTimeout(300);283 const entries = popup.locator(".peach-entry-row");284 const count = await entries.count();285 expect(count).toBe(1);286 await ctx.close();287 });288});289290test.describe("Search - Advanced", () => {291 let ctx: BrowserContext;292 test.afterEach(async () => { await ctx.close(); });293 test.beforeEach(async () => { ctx = await makeContext(); });294295 test("SA01 - search with spaces works", async () => {296 const { popup } = await setupVault(ctx);297 await createEntry(popup, "My Test Entry");298 await popup.locator('input[placeholder="search vault..."]').fill("Test");299 await popup.waitForTimeout(300);300 const entries = popup.locator(".peach-entry-row");301 const count = await entries.count();302 expect(count).toBe(1);303 await ctx.close();304 });305306 test("SA02 - search remembers previous query", async () => {307 const { popup } = await setupVault(ctx);308 await createEntry(popup, "Test1");309 await popup.locator('input[placeholder="search vault..."]').fill("Test1");310 await popup.waitForTimeout(300);311 await popup.locator(".peach-entry-name-button").first().click();312 await popup.waitForTimeout(300);313 await popup.keyboard.press("Escape");314 await popup.waitForTimeout(200);315 const value = await popup.locator('input[placeholder="search vault..."]').inputValue();316 expect(value).toBe("Test1");317 await ctx.close();318 });319320 test("SA03 - search field does not accept invalid characters", async () => {321 const { popup } = await setupVault(ctx);322 await createEntry(popup, "Test");323 const searchInput = popup.locator('input[placeholder="search vault..."]');324 await searchInput.fill("Test\r\n");325 await ctx.close();326 });327328 test("SA04 - search with very long query", async () => {329 const { popup } = await setupVault(ctx);330 await createEntry(popup, "Short");331 const longQuery = "a".repeat(100);332 await popup.locator('input[placeholder="search vault..."]').fill(longQuery);333 await popup.waitForTimeout(300);334 await ctx.close();335 });336337 test("SA05 - search in non-latin characters", async () => {338 const { popup } = await setupVault(ctx);339 await createEntry(popup, "Тест");340 await popup.locator('input[placeholder="search vault..."]').fill("Тест");341 await popup.waitForTimeout(300);342 await ctx.close();343 });344});