Files
llm-browser/frontend/tests/routing.test.tsx
T
2026-10-19 16:04:17 +02:00

144 lines
4.7 KiB
TypeScript

import { fireEvent, render, screen, within } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import App from '../src/App'
import {
AppStateProvider,
PATTERNS_PATH,
activeHref,
navigationGroups,
patternFromSearch,
routeForPath,
routes,
} from '../src/app'
function renderAt(url: string) {
window.history.pushState(null, '', url)
return render(
<AppStateProvider>
<App />
</AppStateProvider>,
)
}
/** The sidebar and the overview cards can link to the same page — scope the
queries that must land on one of them. */
const sidebar = () => within(document.querySelector('.sidebar') as HTMLElement)
const overview = () => within(document.querySelector('.overview-grid') as HTMLElement)
beforeEach(() => {
localStorage.clear()
/* jsdom has no layout — the shell's scroll-to-top effect would hit the
"not implemented" hook on every navigation. */
vi.spyOn(window, 'scrollTo').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('route table', () => {
it('keeps every sidebar entry pointed at a route that exists', () => {
const known = new Set(routes.map(route => route.path))
for (const group of navigationGroups) {
for (const item of group.items) {
const [path] = item.href.split('?')
expect(known.has(path!)).toBe(true)
}
}
})
it('leaves no route unreachable from the sidebar', () => {
const navPaths = new Set(
navigationGroups.flatMap(group => group.items.map(item => item.href.split('?')[0]!)),
)
for (const route of routes) {
expect(navPaths.has(route.path)).toBe(true)
}
})
it('looks routes up by path and rejects unknown ones', () => {
expect(routeForPath('/components/buttons')?.label).toBe('Buttons')
expect(routeForPath('/nope')).toBeUndefined()
})
})
describe('pattern URLs', () => {
it('accepts a known pattern, falls back on anything else', () => {
expect(patternFromSearch('?pattern=table')).toBe('table')
expect(patternFromSearch('?pattern=space')).toBe('metrics')
expect(patternFromSearch('')).toBe('metrics')
})
it('marks the matching pattern entry active', () => {
expect(activeHref(PATTERNS_PATH, '?pattern=deploy')).toBe('/patterns?pattern=deploy')
expect(activeHref(PATTERNS_PATH, '')).toBe('/patterns?pattern=metrics')
expect(activeHref('/components/icons', '')).toBe('/components/icons')
})
})
describe('routing', () => {
it('renders the overview at /', () => {
renderAt('/')
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Build with clarity.')
expect(overview().getByText('Foundations')).toBeInTheDocument()
expect(overview().getByRole('link', { name: /Color tokens/ })).toHaveAttribute(
'href',
'/foundations/colors',
)
})
it('renders the page registered for a URL', () => {
for (const route of routes) {
const { unmount } = renderAt(route.path)
expect(screen.getByText(route.title, { selector: '.section-heading h2' })).toBeInTheDocument()
unmount()
}
})
it('marks the sidebar entry for the current URL as current', () => {
renderAt('/foundations/colors')
expect(sidebar().getByRole('link', { name: 'Color tokens' })).toHaveAttribute('aria-current', 'page')
expect(sidebar().getByRole('link', { name: 'Typography' })).not.toHaveAttribute('aria-current')
})
it('navigates on a sidebar click without a full page load', () => {
renderAt('/')
const link = sidebar().getByRole('link', { name: 'Typography' })
fireEvent.click(link)
expect(window.location.pathname).toBe('/foundations/typography')
expect(link).toHaveAttribute('aria-current', 'page')
expect(screen.getByText('Typography', { selector: '.section-heading h2' })).toBeInTheDocument()
})
it('opens the pattern named in the URL', () => {
renderAt('/patterns?pattern=table')
expect(screen.getByRole('button', { name: /Service Monitor Table/ })).toHaveAttribute('aria-pressed', 'true')
expect(screen.getByRole('button', { name: /Metrics Dashboard/ })).toHaveAttribute('aria-pressed', 'false')
// Sidebar pattern entries share /patterns and differ only by the query.
expect(sidebar().getByRole('link', { name: 'Service Monitor Table' })).toHaveAttribute(
'aria-current',
'page',
)
})
it('writes the chosen pattern into the URL', () => {
renderAt('/patterns')
fireEvent.click(screen.getByRole('button', { name: /Deployment Flow/ }))
expect(window.location.search).toBe('?pattern=deploy')
})
it('falls back to a 404 page instead of a blank screen', () => {
renderAt('/definitely/not/a/route')
expect(screen.getByText('Page not found')).toBeInTheDocument()
})
})