Co-Authored-By: Claude Haiku 4.5 <[email protected]>
145 lines
4.1 KiB
TypeScript
145 lines
4.1 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 { taskFixtures } from './page-fixtures';
|
|
|
|
const { apiMock } = vi.hoisted(() => ({
|
|
apiMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/lib/api', () => ({
|
|
api: apiMock,
|
|
}));
|
|
|
|
import { TasksPage } from './tasks';
|
|
|
|
interface Deferred<T> {
|
|
promise: Promise<T>;
|
|
resolve: (value: T) => void;
|
|
}
|
|
|
|
function createDeferred<T>(): Deferred<T> {
|
|
let resolve!: (value: T) => void;
|
|
const promise = new Promise<T>((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<void> {
|
|
const routes: RouteObject[] = [{ path: '/tasks', element: <TasksPage /> }];
|
|
const router = createMemoryRouter(routes, { initialEntries: ['/tasks'] });
|
|
container = document.createElement('div');
|
|
document.body.append(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(<RouterProvider router={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 }));
|
|
}
|
|
|
|
describe('TasksPage', () => {
|
|
it('shows a visible loading state before the tasks request settles', async () => {
|
|
const deferred = createDeferred<typeof taskFixtures>();
|
|
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');
|
|
});
|
|
});
|