/** * AI Software Factory — E2E: Search (Phase 9) * * Covers: unique-text search < 5s, type filtering, recent searches, and * live suggestions while typing. * * Selector convention (tests/README.md): * data-testid="search-button" top-bar search button * data-testid="search-dialog" search overlay * data-testid="search-input" search query input * data-testid="search-result-{id}" a result row * data-testid="search-result-type-{id}" the type badge on a result row * data-testid="search-filter-type" type filter dropdown * data-testid="search-recent" "Recent searches" section * data-testid="search-recent-item" a recent search chip * data-testid="search-suggestion" a live suggestion row */ import { test, expect } from "../fixtures"; import { createPage } from "../utils/api"; import { waitForCondition } from "../utils/wait"; const unique = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; test.describe("Search — Phase 9", () => { test("create page with unique text → /search finds it in < 5s", async ({ authPage, api, freshWorkspace, }) => { const ws = await freshWorkspace("Search Space"); const token = unique("ZEBRAQUINCE"); await createPage(api, ws.id, { title: `${token} Quarterly Review` }); // Open the search UI (top bar → ⌘K or the search button). await authPage.goto(`/w/${ws.id}`); await authPage.getByTestId("search-button").click(); await expect(authPage.getByTestId("search-dialog")).toBeVisible(); // Search; assert a result within 5 seconds (indexing can lag the write). const input = authPage.getByTestId("search-input"); const started = Date.now(); await input.fill(token); await waitForCondition( authPage, () => authPage.getByTestId("search-result").first().isVisible(), { timeoutMs: 5_000, message: `no search result for ${token} within 5s` }, ); const elapsedMs = Date.now() - started; expect(elapsedMs, `search took ${elapsedMs}ms (target < 5000ms)`).toBeLessThan(5_000); const result = authPage.getByTestId("search-result").first(); await expect(result).toContainText(token); }); test("filter results by type (page vs database)", async ({ authPage, api, freshWorkspace, }) => { const ws = await freshWorkspace("Search Filter Space"); const token = unique("FILTERME"); await createPage(api, ws.id, { title: `${token} Page Doc` }); // A database is a PAGE of type "database" — there is no // `POST /workspaces/:id/databases` route (recorded conflict; the old setup // hit that non-existent route and 404'd). The page's kind surfaces as the // "database" search type facet. const dbRes = await api.post(`/api/v1/workspaces/${ws.id}/pages`, { data: { title: `${token} Data Table`, type: "database" }, }); expect(dbRes.status()).toBe(201); await authPage.goto(`/w/${ws.id}`); await authPage.getByTestId("search-button").click(); await authPage.getByTestId("search-input").fill(token); await expect(authPage.getByTestId("search-result").first()).toBeVisible({ timeout: 10_000, }); // Both types visible initially. await expect(authPage.getByTestId("search-result-type-page").first()).toBeVisible({ timeout: 10_000, }); await expect( authPage.getByTestId("search-result-type-database").first(), ).toBeVisible({ timeout: 10_000 }); // Filter to pages only → the database result disappears. await authPage.getByTestId("search-filter-type").click(); await authPage.getByTestId("search-filter-option-page").click(); await expect(authPage.getByTestId("search-result-type-page").first()).toBeVisible(); await expect(authPage.getByTestId("search-result-type-database")).toHaveCount(0); // Filter to databases only → the reverse. await authPage.getByTestId("search-filter-type").click(); await authPage.getByTestId("search-filter-option-database").click(); await expect( authPage.getByTestId("search-result-type-database").first(), ).toBeVisible(); await expect(authPage.getByTestId("search-result-type-page")).toHaveCount(0); }); test("recent searches are shown after closing the dialog", async ({ authPage, api, freshWorkspace, }) => { const ws = await freshWorkspace("Recent Space"); const token = unique("RECENT"); await createPage(api, ws.id, { title: `${token} Doc` }); await authPage.goto(`/w/${ws.id}`); // First search session. await authPage.getByTestId("search-button").click(); await authPage.getByTestId("search-input").fill(token); await expect(authPage.getByTestId("search-result").first()).toBeVisible({ timeout: 10_000, }); await authPage.keyboard.press("Escape"); // close dialog // Reopen → the query appears under "Recent searches". await authPage.getByTestId("search-button").click(); await expect(authPage.getByTestId("search-recent")).toBeVisible({ timeout: 5_000 }); const recentItem = authPage.getByTestId("search-recent-item").filter({ hasText: token }); await expect(recentItem).toBeVisible(); // Clicking the chip re-runs the search. await recentItem.click(); await expect(authPage.getByTestId("search-result").first()).toBeVisible({ timeout: 10_000, }); }); test("suggestions appear while typing", async ({ authPage, api, freshWorkspace, }) => { const ws = await freshWorkspace("Suggest Space"); const token = unique("SUGGEST"); await createPage(api, ws.id, { title: `${token} Alpha Report` }); await createPage(api, ws.id, { title: `${token} Beta Report` }); await authPage.goto(`/w/${ws.id}`); await authPage.getByTestId("search-button").click(); const input = authPage.getByTestId("search-input"); // Type incrementally; the suggestions list should react to the prefix. await input.pressSequentially(token, { delay: 30 }); const suggestions = authPage.getByTestId("search-suggestion"); await expect .poll(async () => suggestions.count(), { timeout: 10_000 }) .toBeGreaterThanOrEqual(1); // Both matching docs should surface (same unique prefix). await expect .poll(async () => suggestions.count(), { timeout: 10_000 }) .toBeGreaterThanOrEqual(2); }); });