import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { createMemoryRouter, RouterProvider, type RouteObject } from 'react-router-dom'; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { projectFixtures } from './page-fixtures'; const { apiMock } = vi.hoisted(() => ({ apiMock: vi.fn(), })); vi.mock('@/lib/api', () => ({ api: apiMock, })); import { ProjectsPage } from './projects'; interface Deferred { promise: Promise; resolve: (value: T) => void; reject: (reason?: unknown) => void; } function createDeferred(): Deferred { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } let root: Root | null = null; let container: HTMLDivElement; beforeAll(() => { Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', { configurable: true, value: true, }); }); afterAll(() => { Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT'); }); afterEach(async () => { await act(async () => { root?.unmount(); }); document.body.replaceChildren(); root = null; apiMock.mockReset(); }); async function renderProjectsPage(): Promise> { const routes: RouteObject[] = [ { path: '/projects', element: }, { path: '/projects/:id', element:

Project detail target

}, ]; const router = createMemoryRouter(routes, { initialEntries: ['/projects'] }); container = document.createElement('div'); document.body.append(container); root = createRoot(container); await act(async () => { root?.render(); }); return router; } describe('ProjectsPage', () => { it('shows a visible loading state while the project request is in flight', async () => { const deferred = createDeferred(); apiMock.mockReturnValueOnce(deferred.promise); await renderProjectsPage(); expect(container.textContent).toContain('Loading projects...'); await act(async () => { deferred.resolve(projectFixtures); await deferred.promise; }); }); it('renders project cards from the API and navigates to a project detail route on click', async () => { apiMock.mockResolvedValueOnce(projectFixtures); const router = await renderProjectsPage(); expect(apiMock).toHaveBeenCalledWith('/api/projects'); expect(container.textContent).toContain('Mosaic Stack'); expect(container.textContent).toContain('Agent Runtime'); const button = [...container.querySelectorAll('button')].find((candidate) => candidate.textContent?.includes('Mosaic Stack'), ); expect(button).toBeTruthy(); await act(async () => { button?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(router.state.location.pathname).toBe('/projects/project-1'); expect(container.textContent).toContain('Project detail target'); }); it('renders the empty state when the API returns no projects', async () => { apiMock.mockResolvedValueOnce([]); await renderProjectsPage(); expect(container.textContent).toContain('No projects yet'); expect(container.textContent).toContain( 'Projects will appear here when created via the gateway API', ); }); it('renders a visible alert when the projects request fails', async () => { apiMock.mockRejectedValueOnce(new Error('Projects are unavailable')); await renderProjectsPage(); const alert = container.querySelector('[role="alert"]'); expect(alert).toBeTruthy(); expect(alert?.textContent).toContain('Projects are unavailable'); }); });