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(); sessionStorage.clear(); }); 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; } function clickButtonByText(text: string): void { const button = [...container.querySelectorAll('button')].find((candidate) => candidate.textContent?.includes(text), ); if (!button) { throw new Error(`Button containing "${text}" not found`); } button.dispatchEvent(new MouseEvent('click', { bubbles: true })); } async function flushAct(): Promise { await act(async () => { await Promise.resolve(); }); } 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.mock.calls[0]?.[0]).toBe('/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 only for a verified empty collection', 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', ); expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe( 'current', ); }); it('renders a failed fetch as an explicit unavailable state, never an empty collection', 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'); expect(alert?.textContent).toContain('not an empty result'); // Negative controls: no healthy empty state and no project cards render // from a failed fetch. expect(container.textContent).not.toContain('No projects yet'); expect(container.textContent).not.toContain('Mosaic Stack'); expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe( 'unavailable', ); }); it('renders an auth failure as unavailable and recovers after retry', async () => { apiMock .mockRejectedValueOnce(Object.assign(new Error('Unauthorized'), { statusCode: 401 })) .mockResolvedValueOnce(projectFixtures); await renderProjectsPage(); const alert = container.querySelector('[role="alert"]'); expect(alert?.textContent).toContain('Unauthorized'); expect(container.textContent).not.toContain('No projects yet'); await act(async () => { clickButtonByText('Retry'); }); await flushAct(); expect(container.querySelector('[role="alert"]')).toBeNull(); expect(container.textContent).toContain('Mosaic Stack'); expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe( 'current', ); }); it('renders a schema-mismatched response as unavailable, never as data', async () => { apiMock.mockResolvedValueOnce({ results: projectFixtures }); await renderProjectsPage(); const alert = container.querySelector('[role="alert"]'); expect(alert?.textContent).toContain('not an empty result'); expect(container.textContent).not.toContain('Mosaic Stack'); expect(container.textContent).not.toContain('No projects yet'); expect(container.querySelector('[data-freshness]')?.getAttribute('data-freshness')).toBe( 'unavailable', ); }); });