88 lines
2.6 KiB
TypeScript
88 lines
2.6 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';
|
|
|
|
const { useSessionMock } = vi.hoisted(() => ({
|
|
useSessionMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/lib/auth-client', () => ({
|
|
useSession: useSessionMock,
|
|
}));
|
|
|
|
import { routes } from '@/routes';
|
|
|
|
beforeAll(() => {
|
|
Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
|
|
configurable: true,
|
|
value: true,
|
|
});
|
|
});
|
|
|
|
afterAll(() => {
|
|
Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT');
|
|
});
|
|
|
|
function Boom(): never {
|
|
throw new Error('render blew up');
|
|
}
|
|
|
|
/** Recursively clones the real exported route table, replacing only the
|
|
* `/chat` route's `element` with `<Boom />` — every other route (including
|
|
* the real `AuthGuard` nesting and the real `/chat` `errorElement`) is left
|
|
* exactly as exported. This is what makes the test fail if a future change
|
|
* removes the real route's `errorElement`, unlike a hand-built independent
|
|
* route tree that could drift from production undetected. */
|
|
function replaceChatElementWithBoom(nodes: RouteObject[]): RouteObject[] {
|
|
return nodes.map((node) => {
|
|
const cloned: RouteObject = { ...node };
|
|
if (cloned.path === '/chat') {
|
|
cloned.element = <Boom />;
|
|
}
|
|
if (cloned.children) {
|
|
cloned.children = replaceChatElementWithBoom(cloned.children);
|
|
}
|
|
return cloned;
|
|
});
|
|
}
|
|
|
|
let root: Root | null;
|
|
let container: HTMLElement;
|
|
|
|
afterEach(async () => {
|
|
await act(async () => {
|
|
root?.unmount();
|
|
});
|
|
document.body.replaceChildren();
|
|
root = null;
|
|
useSessionMock.mockReset();
|
|
});
|
|
|
|
describe('ChatRouteErrorBoundary', () => {
|
|
it('renders a recoverable, non-blank fallback when the /chat route element throws during render', async () => {
|
|
useSessionMock.mockReturnValue({ data: { user: { id: 'user-1' } }, isPending: false });
|
|
|
|
const routeObjects = replaceChatElementWithBoom(routes);
|
|
const router = createMemoryRouter(routeObjects, { initialEntries: ['/chat'] });
|
|
|
|
container = document.createElement('div');
|
|
document.body.append(container);
|
|
root = createRoot(container);
|
|
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
try {
|
|
await act(async () => {
|
|
root?.render(<RouterProvider router={router} />);
|
|
});
|
|
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
} finally {
|
|
consoleErrorSpy.mockRestore();
|
|
}
|
|
|
|
expect(container.textContent).not.toBe('');
|
|
expect(container.querySelector('[role="alert"]')).toBeTruthy();
|
|
});
|
|
});
|