Co-Authored-By: Claude Haiku 4.5 <[email protected]>
132 lines
3.7 KiB
TypeScript
132 lines
3.7 KiB
TypeScript
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<T> {
|
|
promise: Promise<T>;
|
|
resolve: (value: T) => void;
|
|
reject: (reason?: unknown) => void;
|
|
}
|
|
|
|
function createDeferred<T>(): Deferred<T> {
|
|
let resolve!: (value: T) => void;
|
|
let reject!: (reason?: unknown) => void;
|
|
const promise = new Promise<T>((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<ReturnType<typeof createMemoryRouter>> {
|
|
const routes: RouteObject[] = [
|
|
{ path: '/projects', element: <ProjectsPage /> },
|
|
{ path: '/projects/:id', element: <p>Project detail target</p> },
|
|
];
|
|
|
|
const router = createMemoryRouter(routes, { initialEntries: ['/projects'] });
|
|
container = document.createElement('div');
|
|
document.body.append(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(<RouterProvider router={router} />);
|
|
});
|
|
|
|
return router;
|
|
}
|
|
|
|
describe('ProjectsPage', () => {
|
|
it('shows a visible loading state while the project request is in flight', async () => {
|
|
const deferred = createDeferred<typeof projectFixtures>();
|
|
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');
|
|
});
|
|
});
|