125 lines
3.5 KiB
TypeScript
125 lines
3.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act, createElement } from 'react';
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
opsEnabled: true,
|
|
push: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('next/navigation', () => ({
|
|
useRouter: () => ({ push: mocks.push }),
|
|
}));
|
|
|
|
vi.mock('@/lib/config/deployment-role', () => ({
|
|
isOpsWorkbenchEnabled: () => mocks.opsEnabled,
|
|
}));
|
|
|
|
vi.mock('@/lib/hooks/use-i18n', () => ({
|
|
useI18n: () => ({
|
|
t: (key: string) => key,
|
|
}),
|
|
}));
|
|
|
|
import { GenerationJobsPanel } from '@/components/classroom/generation-jobs-panel';
|
|
|
|
(
|
|
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
let root: Root | null = null;
|
|
let container: HTMLDivElement | null = null;
|
|
|
|
async function renderPanel() {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
await act(async () => {
|
|
root?.render(createElement(GenerationJobsPanel));
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
});
|
|
}
|
|
|
|
function jsonResponse(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
afterEach(async () => {
|
|
if (root) {
|
|
await act(async () => root?.unmount());
|
|
}
|
|
container?.remove();
|
|
root = null;
|
|
container = null;
|
|
mocks.opsEnabled = true;
|
|
mocks.push.mockReset();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('GenerationJobsPanel deployment boundary', () => {
|
|
it('does not request the operations course API on a learner deployment', async () => {
|
|
mocks.opsEnabled = false;
|
|
const fetchMock = vi.fn(async () =>
|
|
jsonResponse({
|
|
items: [
|
|
{
|
|
jobId: 'job-1',
|
|
status: 'queued',
|
|
step: 'queued',
|
|
progress: 0,
|
|
message: 'Waiting',
|
|
createdAt: '2026-08-15T00:00:00.000Z',
|
|
requirement: 'Learner single course',
|
|
scenesGenerated: 0,
|
|
done: false,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await renderPanel();
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/generate-classroom?limit=10');
|
|
expect(container?.textContent).toContain('Learner single course');
|
|
});
|
|
|
|
it('keeps single-courseware jobs visible when the operations course API returns 403', async () => {
|
|
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = String(input);
|
|
if (url.startsWith('/api/generate-classroom')) {
|
|
return jsonResponse({
|
|
items: [
|
|
{
|
|
jobId: 'job-2',
|
|
status: 'succeeded',
|
|
step: 'done',
|
|
progress: 100,
|
|
message: 'Done',
|
|
createdAt: '2026-08-15T00:00:00.000Z',
|
|
requirement: 'Completed single course',
|
|
scenesGenerated: 3,
|
|
totalScenes: 3,
|
|
result: { classroomId: 'classroom-2', url: '/classroom/classroom-2', scenesCount: 3 },
|
|
done: true,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
return jsonResponse({ success: false, errorCode: 'FORBIDDEN' }, 403);
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await renderPanel();
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/courses?limit=5');
|
|
expect(container?.textContent).toContain('Completed single course');
|
|
expect(container?.textContent).not.toContain('jobs.loadFailed');
|
|
});
|
|
});
|