dorfteich/apps/web/e2e/search.spec.ts
Claude Opus 4.8 19fb24c527
All checks were successful
CD / Build and push images (push) Successful in 3m24s
CI / Lint, typecheck, test (push) Successful in 2m16s
CI / Auth e2e pack (push) Successful in 2m52s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
Add search UI with scoping and snippets (#50)
A search palette over the #49 full-text search.

- web:
  - `SearchPalette`: opened from a top-bar button or the global "/" shortcut
    (ignored while typing in a field). Scoped to the current pond by default
    with an "all my ponds" toggle and, when scoped, a label filter. Results
    list title, pond, label chips, and a highlighted snippet; recent searches
    (localStorage) show before typing; empty/error/hint states.
  - `HighlightedSnippet` renders the match — the api wraps hits in shared
    sentinels (private-use codepoints), split here into `<mark>` so no HTML
    from the content is interpreted.
  - Fully keyboard-operable: "/" opens, ↑/↓ move, Enter opens the page,
    Esc closes.
  - i18n `search` namespace (de + en); palette + result styles.
- api: a pondId scope test proves search narrows to one pond.
- e2e `search.spec.ts` (new CI pack): body content added via the editor is
  found and highlighted, the scope toggle keeps the result, and Enter opens
  the page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
2026-07-09 13:36:06 +02:00

66 lines
2.8 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { contextForUser } from './helpers';
/**
* Search UI pack (issue #50). Adds body content to a page through the editor
* (indexed by #49 via the collab persistence hook), then opens the search
* palette with the "/" shortcut and checks that the content is found, the match
* is highlighted, the scope toggle works, and Enter opens the page.
* Language-independent selectors (CSS classes + unique content).
*/
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
type Ctx = Awaited<ReturnType<typeof contextForUser>>;
async function personalPond(context: Ctx): Promise<{ id: string; slug: string }> {
const ponds = await context.request.get('/api/v1/ponds');
const pond = (await ponds.json()).find((p: { type: string }) => p.type === 'personal');
return { id: pond.id, slug: pond.slug };
}
test('search finds page content, highlights it, and is keyboard-operable', async ({ browser }) => {
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
const pond = await personalPond(context);
const ts = Date.now();
const word = `srchword${ts}`;
const title = `Search Page ${ts}`;
const created = await (
await context.request.post(`/api/v1/ponds/${pond.id}/pages`, { data: { title } })
).json();
const page = await context.newPage();
await page.goto(`/p/${pond.slug}/${created.slug}`);
// Add body content through the editor, then reload to flush it to the index.
await page.locator('.editor-page__mode-toggle').click();
const body = page.locator('.editor-content .ProseMirror');
await body.click();
await page.keyboard.type(`the unique ${word} lives in this body`);
await page.reload();
await expect(page.locator('.editor-content')).toContainText(word);
// Leave edit mode so "/" is a shortcut, not editor input.
await page.locator('.editor-page__mode-toggle').click();
// Open search with the "/" shortcut.
await page.keyboard.press('/');
const palette = page.locator('.search-palette');
await expect(palette).toBeVisible();
await palette.locator('.search-palette__input').fill(word);
const result = page.locator('.search-result', { hasText: title });
await expect(result).toBeVisible();
// The matched word is highlighted in the snippet.
await expect(page.locator('.search-result__snippet mark')).toBeVisible();
// Scope toggle is present and keeps the result (page is in the user's ponds).
await palette.locator('.search-palette__scope input[type="checkbox"]').first().check();
await expect(page.locator('.search-result', { hasText: title })).toBeVisible();
// Enter opens the selected result.
await palette.locator('.search-palette__input').press('Enter');
await expect(page).toHaveURL(new RegExp(`/p/${pond.slug}/${created.slug}$`));
await context.close();
});