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 { taskFixtures } from './page-fixtures'; const { apiMock } = vi.hoisted(() => ({ apiMock: vi.fn(), })); vi.mock('@/lib/api', () => ({ api: apiMock, })); import { TasksPage } from './tasks'; interface Deferred { promise: Promise; resolve: (value: T) => void; } function createDeferred(): Deferred { let resolve!: (value: T) => void; const promise = new Promise((res) => { resolve = res; }); return { promise, resolve }; } 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 renderTasksPage(): Promise { const routes: RouteObject[] = [{ path: '/tasks', element: }]; const router = createMemoryRouter(routes, { initialEntries: ['/tasks'] }); container = document.createElement('div'); document.body.append(container); root = createRoot(container); await act(async () => { root?.render(); }); } 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 })); } describe('TasksPage', () => { it('shows a visible loading state before the tasks request settles', async () => { const deferred = createDeferred(); apiMock.mockReturnValueOnce(deferred.promise); await renderTasksPage(); expect(container.textContent).toContain('Loading tasks...'); await act(async () => { deferred.resolve(taskFixtures); await deferred.promise; }); }); it('starts in kanban view, toggles to list view, and opens the read-only modal from cards and rows', async () => { apiMock.mockResolvedValueOnce(taskFixtures); await renderTasksPage(); expect(container.textContent).toContain('Not Started'); expect(container.textContent).toContain('In Progress'); expect(container.textContent).toContain('Blocked'); const kanbanCard = [...container.querySelectorAll('button')].find((candidate) => candidate.textContent?.includes('Route /projects/:id'), ); expect(kanbanCard).toBeTruthy(); await act(async () => { kanbanCard?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(container.querySelector('[role="dialog"]')).toBeTruthy(); await act(async () => { container .querySelector('button[aria-label="Close task details"]') ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(container.querySelector('[role="dialog"]')).toBeNull(); await act(async () => { clickButtonByText('List'); }); const row = [...container.querySelectorAll('tr')].find((candidate) => candidate.textContent?.includes('Route /tasks'), ); expect(row).toBeTruthy(); await act(async () => { row?.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(container.querySelector('[role="dialog"]')).toBeTruthy(); expect(container.textContent).toContain('Wire list and kanban modal interactions'); }); it('renders a visible alert when the tasks request fails', async () => { apiMock.mockRejectedValueOnce(new Error('Tasks request failed')); await renderTasksPage(); const alert = container.querySelector('[role="alert"]'); expect(alert).toBeTruthy(); expect(alert?.textContent).toContain('Tasks request failed'); }); });