Files
textbee/web/e2e/search.spec.ts
isra el 02bd1d42e0 feat: make every page searchable with keyword matching
Search previously listed only the 5 sidebar routes plus Log out, so subroutes
like bulk send, message history, webhook deliveries and every account section
were unreachable by search. Typing "csv" or "invoice" or "password" returned
nothing.

New search-registry.ts is the single source of truth: every user-reachable
destination with a description and the words people actually type, rather than
the labels we happened to choose for the nav. cmdk matches those keywords in
addition to the label, so "csv" finds Bulk send and "invoice" finds Billing.
External resources (Android app, quick start, status, contribute) are included
and open in a new tab.

Mobile had no search at all: the trigger and the dialog were one component
living inside the `hidden md:flex` sidebar. Split them. The dialog now mounts
in the dashboard layout with open state lifted, the sidebar keeps its trigger,
and mobile gets a labelled sticky search bar. That matters most on mobile,
where the tab bar is capped at 4 items and search is the only path to Webhooks
and every subroute.

The palette also gains theme actions, since the theme control now lives in the
desktop-only sidebar.

search-registry.test.ts walks the App Router tree on disk and fails if any
page.tsx has no registry entry, naming the offending route. Redirect-only
routes are allowlisted. Verified it actually fails by removing an entry, rather
than assuming a green test means coverage. Plus e2e proving keyword search,
the mobile path to webhooks, and the keyboard shortcut.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:26:33 +03:00

85 lines
3.0 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { authenticate } from './session'
import { mockApi } from './mock-api'
// Search must find pages by the words a user thinks in, not by our nav labels,
// and it must be reachable on mobile (the trigger used to live only in the
// desktop-only sidebar).
test.describe('command palette search (mocked API, no real backend)', () => {
test('desktop: keyword search reaches a subroute the sidebar does not list', async ({
page,
context,
}) => {
await page.setViewportSize({ width: 1280, height: 900 })
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard')
await page.getByRole('button', { name: /search/i }).first().click()
// "csv" appears nowhere in the label "Bulk send"; it matches via keywords.
await page.getByPlaceholder(/search pages/i).fill('csv')
await page.getByRole('option', { name: /bulk send/i }).click()
await expect(page).toHaveURL(/\/dashboard\/messaging\/bulk/)
})
test('desktop: business wording finds billing', async ({ page, context }) => {
await page.setViewportSize({ width: 1280, height: 900 })
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard')
await page.getByRole('button', { name: /search/i }).first().click()
await page.getByPlaceholder(/search pages/i).fill('invoice')
await page.getByRole('option', { name: /billing/i }).click()
await expect(page).toHaveURL(/\/dashboard\/account\/billing/)
})
test('mobile: search is reachable and reaches webhooks', async ({
page,
context,
}) => {
await page.setViewportSize({ width: 375, height: 800 })
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard')
// Webhooks is deliberately absent from the 4-item mobile tab bar, so
// search is the mobile path to it.
await page.getByRole('button', { name: /search/i }).first().click()
await page.getByPlaceholder(/search pages/i).fill('callback')
await page.getByRole('option', { name: /^webhooks/i }).first().click()
await expect(page).toHaveURL(/\/dashboard\/webhooks/)
})
test('keyboard shortcut opens the palette', async ({ page, context }) => {
await page.setViewportSize({ width: 1280, height: 900 })
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard')
await page.keyboard.press('ControlOrMeta+k')
await expect(page.getByPlaceholder(/search pages/i)).toBeVisible()
})
test('theme can be changed from the palette (the mobile path)', async ({
page,
context,
}) => {
await page.setViewportSize({ width: 375, height: 800 })
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard')
await page.getByRole('button', { name: /search/i }).first().click()
await page.getByPlaceholder(/search pages/i).fill('dark')
await page.getByRole('option', { name: /dark theme/i }).click()
await expect(page.locator('html')).toHaveClass(/dark/)
})
})