search.spec.ts

12.8 KB · raw

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