feat: add sign-in flow, routing shell, and searchable book list

Pin @testing-library/jest-dom to ~6.9.1 (6.10.0 is npm-deprecated).

Claude-Session: https://claude.ai/code/session_01M9MLit5Ko3X4s7rzC5Kv7X
This commit is contained in:
2026-10-02 14:23:42 -05:00
parent e1096da1a7
commit f0efa25f23
14 changed files with 625 additions and 11 deletions
+65
View File
@@ -0,0 +1,65 @@
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter } from 'react-router'
import { vi } from 'vitest'
import type { Book, Genre, User } from '../api'
import { AppRoutes } from '../App'
import { AuthProvider } from '../auth/AuthContext'
export const alice: User = { id: 1, username: 'alice' }
export const genres: Genre[] = [
{ id: 3, name: 'Fantasy' },
{ id: 5, name: 'Science Fiction' },
]
export const dune: Book = {
id: 7,
title: 'Dune',
author: 'Frank Herbert',
genre: { id: 5, name: 'Science Fiction' },
total_pages: 380,
current_page: 142,
status: 'reading',
created_at: '2026-10-01T10:00:00+00:00',
updated_at: '2026-10-01T10:00:00+00:00',
}
type Reply = [status: number, body?: unknown]
type Handler = (request: { body: unknown; url: URL }) => Reply
export type Call = { method: string; path: string; body: unknown }
/** Replaces fetch (the network boundary) with route handlers keyed by "METHOD /api/path". */
export function mockApi(routes: Record<string, Handler>): Call[] {
const calls: Call[] = []
vi.stubGlobal(
'fetch',
vi.fn(async (input: string, init: RequestInit = {}) => {
const url = new URL(input, 'http://localhost')
const method = init.method ?? 'GET'
const body = typeof init.body === 'string' ? JSON.parse(init.body) : undefined
calls.push({ method, path: url.pathname + url.search, body })
const handler = routes[`${method} ${url.pathname}`]
const [status, data] = handler ? handler({ body, url }) : [500, { error: `No mock for ${method} ${url.pathname}` }]
return new Response(status === 204 ? null : JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' },
})
}),
)
return calls
}
export function signedIn(routes: Record<string, Handler> = {}): Call[] {
return mockApi({ 'GET /api/auth/me': () => [200, alice], 'GET /api/genres': () => [200, genres], ...routes })
}
export function renderApp(url: string) {
const user = userEvent.setup()
render(
<MemoryRouter initialEntries={[url]}>
<AuthProvider>
<AppRoutes />
</AuthProvider>
</MemoryRouter>,
)
return user
}