search.spec.ts

10.3 KB · raw

1import { test, expect, Page, BrowserContext } from "@playwright/test";2import { UNLOCKED, VAULT_PASSWORD, addEntry, listSearch, makeContext, openList, setupVault } from "./helpers/vault";34// The top bar's search field is readOnly — it only opens the list overlay,5// where the editable field lives. Specs that filled the top field were6// filling a readOnly input.7const topSearch = (popup: Page) => popup.locator(".peach-search-wrap .peach-search-input");89// Real vault creation runs Argon2id; seeded entries need a popup reload each.10test.describe.configure({ timeout: 90_000 });1112// Entries are added through the background, which the open popup does not13// observe — reload so the list re-reads the vault.14async function createEntry(popup: Page, name: string, domain = "test.com") {15  await addEntry(popup, {16    title: name,17    url: `https://${domain}`,18    username: `${name.toLowerCase()}@${domain}`,19    password: "Passphrase123!",20  });21  await popup.reload();22  // The reload lands on either the lock screen or the vault; waiting for one23  // of them first avoids racing the render.24  await popup.waitForSelector(`#unlock-password, ${UNLOCKED}`, { timeout: 30000 });25  const unlockInput = popup.locator("#unlock-password");26  if (await unlockInput.isVisible().catch(() => false)) {27    await unlockInput.fill(VAULT_PASSWORD);28    await popup.getByRole("button", { name: /unlock/i }).click();29  }30  await expect(popup.locator(UNLOCKED)).toBeVisible({ timeout: 30000 });31  await openList(popup);32}3334test.describe("Search", () => {35  let ctx: BrowserContext;36  test.afterEach(async () => { await ctx.close(); });37  test.beforeEach(async () => { ctx = await makeContext(); });3839  test("S01 - search input is visible", async () => {40    const { popup } = await setupVault(ctx);41    await expect(topSearch(popup)).toBeVisible();42    await ctx.close();43  });4445  test("S02 - search input has placeholder text", async () => {46    const { popup } = await setupVault(ctx);47    const placeholder = await topSearch(popup).getAttribute("placeholder");48    expect(placeholder).toContain("search");49    await ctx.close();50  });5152  test("S03 - search filters entries by name", async () => {53    const { popup } = await setupVault(ctx);54    await createEntry(popup, "GoogleEntry");55    await createEntry(popup, "AmazonEntry");56    await listSearch(popup).fill("Google");57    await popup.waitForTimeout(300);58    const entries = popup.locator(".peach-entry-row");59    const count = await entries.count();60    expect(count).toBe(1);61    await ctx.close();62  });6364  test("S04 - search is case-insensitive", async () => {65    const { popup } = await setupVault(ctx);66    await createEntry(popup, "TestEntry");67    await listSearch(popup).fill("test");68    await popup.waitForTimeout(300);69    const entries = popup.locator(".peach-entry-row");70    const count = await entries.count();71    expect(count).toBe(1);72    await ctx.close();73  });7475  test("S05 - search with no results shows empty state", async () => {76    const { popup } = await setupVault(ctx);77    await createEntry(popup, "Entry1");78    await listSearch(popup).fill("nonexistent");79    // The list's empty state is .peach-list-empty ("no entries yet"); there is80    // no separate "no results" copy for a filtered-to-nothing search.81    await expect(popup.locator(".peach-list-empty")).toBeVisible({ timeout: 5000 });82    await expect(popup.locator(".peach-entry-row")).toHaveCount(0);83    await ctx.close();84  });8586  test("S06 - clearing search shows all entries", async () => {87    const { popup } = await setupVault(ctx);88    await createEntry(popup, "EntryOne");89    await createEntry(popup, "EntryTwo");90    await listSearch(popup).fill("EntryOne");91    await popup.waitForTimeout(300);92    await listSearch(popup).clear();93    await popup.waitForTimeout(300);94    const entries = popup.locator(".peach-entry-row");95    const count = await entries.count();96    expect(count).toBe(2);97    await ctx.close();98  });99100  test("S07 - search by domain works", async () => {101    const { popup } = await setupVault(ctx);102    await createEntry(popup, "Site1", "google.com");103    await createEntry(popup, "Site2", "amazon.com");104    await listSearch(popup).fill("google");105    await popup.waitForTimeout(300);106    const entries = popup.locator(".peach-entry-row");107    const count = await entries.count();108    expect(count).toBe(1);109    await ctx.close();110  });111112  test("S08 - search by username works", async () => {113    const { popup } = await setupVault(ctx);114    await createEntry(popup, "User1");115    await createEntry(popup, "User2");116    await listSearch(popup).fill("[email protected]");117    await popup.waitForTimeout(300);118    const entries = popup.locator(".peach-entry-row");119    const count = await entries.count();120    expect(count).toBe(1);121    await ctx.close();122  });123124  test("S09 - search preserves scroll position", async () => {125    const { popup } = await setupVault(ctx);126    for (let i = 0; i < 15; i++) {127      await createEntry(popup, `Entry${i}`, `site${i}.com`);128    }129    await listSearch(popup).fill("Entry5");130    await popup.waitForTimeout(300);131    await listSearch(popup).clear();132    await popup.waitForTimeout(300);133    await ctx.close();134  });135136  test("S10 - search with special characters", async () => {137    const { popup } = await setupVault(ctx);138    await createEntry(popup, "Test@Entry");139    await listSearch(popup).fill("@");140    await popup.waitForTimeout(300);141    await ctx.close();142  });143144  test("S11 - search with regex characters", async () => {145    const { popup } = await setupVault(ctx);146    await createEntry(popup, "Test.Entry");147    await listSearch(popup).fill(".");148    await popup.waitForTimeout(300);149    await ctx.close();150  });151152  test("S12 - search input has proper aria-label", async () => {153    const { popup } = await setupVault(ctx);154    const ariaLabel = await topSearch(popup).getAttribute("aria-label");155    expect(ariaLabel).toBeTruthy();156    await ctx.close();157  });158159  test("S13 - escape closes the list overlay", async () => {160    const { popup } = await setupVault(ctx);161    await createEntry(popup, "TestEntry");162    await listSearch(popup).fill("Test");163    await popup.keyboard.press("Escape");164    // Escape dismisses the list rather than emptying the field.165    await expect(listSearch(popup)).toHaveCount(0, { timeout: 5000 });166    await ctx.close();167  });168169  test("S14 - search debounce works", async () => {170    const { popup } = await setupVault(ctx);171    await createEntry(popup, "TestEntry");172    const searchInput = listSearch(popup);173    await searchInput.fill("T");174    await popup.waitForTimeout(100);175    await searchInput.fill("Te");176    await popup.waitForTimeout(100);177    await searchInput.fill("Tes");178    await popup.waitForTimeout(300);179    await ctx.close();180  });181182  test("S15 - search highlights matching text", async () => {183    const { popup } = await setupVault(ctx);184    await createEntry(popup, "TestingEntry");185    await listSearch(popup).fill("Test");186    await popup.waitForTimeout(300);187    const highlighted = popup.locator("[class*='highlight' i], [class*='match' i]").first();188    const hasHighlight = await highlighted.isVisible().catch(() => false);189    if (hasHighlight) expect(true).toBeTruthy();190    await ctx.close();191  });192193  test("S16 - empty search shows all entries", async () => {194    const { popup } = await setupVault(ctx);195    await createEntry(popup, "Entry1");196    await createEntry(popup, "Entry2");197    await listSearch(popup).fill("");198    await popup.waitForTimeout(300);199    const entries = popup.locator(".peach-entry-row");200    const count = await entries.count();201    expect(count).toBe(2);202    await ctx.close();203  });204205  test("S17 - search accessible via keyboard", async () => {206    const { popup } = await setupVault(ctx);207    await createEntry(popup, "TestEntry");208    await popup.keyboard.press("/");209    await popup.waitForTimeout(200);210    const overlaySearch = popup.locator(".peach-overlay-view .peach-search-input");211    await expect(overlaySearch).toBeVisible();212    const focused = await overlaySearch.evaluate((el) => document.activeElement === el);213    expect(focused).toBeTruthy();214    await ctx.close();215  });216217  test("S18 - partial match works", async () => {218    const { popup } = await setupVault(ctx);219    await createEntry(popup, "MyPasswordEntry");220    await listSearch(popup).fill("Pass");221    await popup.waitForTimeout(300);222    const entries = popup.locator(".peach-entry-row");223    const count = await entries.count();224    expect(count).toBe(1);225    await ctx.close();226  });227});228229test.describe("Search - Advanced", () => {230  let ctx: BrowserContext;231  test.afterEach(async () => { await ctx.close(); });232  test.beforeEach(async () => { ctx = await makeContext(); });233234  test("SA01 - search with spaces works", async () => {235    const { popup } = await setupVault(ctx);236    await createEntry(popup, "My Test Entry");237    await listSearch(popup).fill("Test");238    await popup.waitForTimeout(300);239    const entries = popup.locator(".peach-entry-row");240    const count = await entries.count();241    expect(count).toBe(1);242    await ctx.close();243  });244245  // The list search does NOT persist across close/reopen — reopening the246  // list starts from an empty query. Asserted as observed.247  test("SA02 - reopening the list starts from an empty query", async () => {248    const { popup } = await setupVault(ctx);249    await createEntry(popup, "Test1");250    await listSearch(popup).fill("Test1");251    await popup.locator(".peach-entry-row").first().click();252    await expect(listSearch(popup)).toHaveCount(0, { timeout: 5000 });253    await openList(popup);254    await expect(listSearch(popup)).toHaveValue("");255    await ctx.close();256  });257258  test("SA03 - search field does not accept invalid characters", async () => {259    const { popup } = await setupVault(ctx);260    await createEntry(popup, "Test");261    const searchInput = listSearch(popup);262    await searchInput.fill("Test\r\n");263    await ctx.close();264  });265266  test("SA04 - search with very long query", async () => {267    const { popup } = await setupVault(ctx);268    await createEntry(popup, "Short");269    const longQuery = "a".repeat(100);270    await listSearch(popup).fill(longQuery);271    await popup.waitForTimeout(300);272    await ctx.close();273  });274275  test("SA05 - search in non-latin characters", async () => {276    const { popup } = await setupVault(ctx);277    await createEntry(popup, "Тест");278    await listSearch(popup).fill("Тест");279    await popup.waitForTimeout(300);280    await ctx.close();281  });282});