import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Learning } from '@/pages/Learning';
import { MemoryRouter } from 'react-router-dom';
const fetchLearningCoursesMock = vi.hoisted(() => vi.fn());
const fetchMyLearningCoursesMock = vi.hoisted(() => vi.fn());
const listInstalledLearningCoursesMock = vi.hoisted(() => vi.fn());
const downloadLearningCourseMock = vi.hoisted(() => vi.fn());
const startLearningGenerationMock = vi.hoisted(() => vi.fn());
const fetchLearningGenerationMock = vi.hoisted(() => vi.fn());
const finalizeLearningGenerationMock = vi.hoisted(() => vi.fn());
const cancelLearningGenerationMock = vi.hoisted(() => vi.fn());
const resumeLearningGenerationMock = vi.hoisted(() => vi.fn());
const transcribeLearningSpeechMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/learning', () => ({
fetchLearningCourses: fetchLearningCoursesMock,
fetchMyLearningCourses: fetchMyLearningCoursesMock,
listInstalledLearningCourses: listInstalledLearningCoursesMock,
downloadLearningCourse: downloadLearningCourseMock,
startLearningGeneration: startLearningGenerationMock,
fetchLearningGeneration: fetchLearningGenerationMock,
finalizeLearningGeneration: finalizeLearningGenerationMock,
cancelLearningGeneration: cancelLearningGenerationMock,
resumeLearningGeneration: resumeLearningGenerationMock,
transcribeLearningSpeech: transcribeLearningSpeechMock,
}));
describe('Learning page', () => {
beforeEach(() => {
fetchLearningCoursesMock.mockReset();
fetchMyLearningCoursesMock.mockReset();
listInstalledLearningCoursesMock.mockReset();
downloadLearningCourseMock.mockReset();
startLearningGenerationMock.mockReset();
fetchLearningGenerationMock.mockReset();
finalizeLearningGenerationMock.mockReset();
cancelLearningGenerationMock.mockReset();
resumeLearningGenerationMock.mockReset();
transcribeLearningSpeechMock.mockReset();
window.localStorage.clear();
listInstalledLearningCoursesMock.mockResolvedValue([]);
fetchMyLearningCoursesMock.mockResolvedValue([]);
});
it('renders published courses returned by Works Square', async () => {
fetchLearningCoursesMock.mockResolvedValue([{
id: 'course-1',
origin: 'ops_large',
status: 'published',
title: 'Python 基础入门',
summary: '通过互动讲解掌握 Python。',
language: 'zh-CN',
contentHash: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
archiveBytes: 1024,
formatVersion: 1,
minPlayerVersion: '1.0.0',
sceneCount: 10,
capabilities: { sceneKinds: ['slide', 'interactive', 'quiz'], hasAudio: false, hasWhiteboard: true, hasAgent: true },
createdAt: '2026-08-16T00:00:00Z',
publishedAt: '2026-08-16T00:00:00Z',
}]);
render();
expect(screen.getByRole('status')).toHaveTextContent('正在读取课程');
expect(await screen.findByTestId('learning-course-course-1')).toHaveTextContent('Python 基础入门');
expect(screen.getByText('10 个学习场景')).toBeInTheDocument();
});
it('shows a retryable error state', async () => {
fetchLearningCoursesMock.mockRejectedValue(new Error('连接失败'));
render();
await waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent('连接失败'));
expect(screen.getByRole('button', { name: '重试' })).toBeEnabled();
});
it('opens the full single-course workbench only from the generate route', async () => {
fetchLearningCoursesMock.mockResolvedValue([]);
render();
await screen.findByText('暂时还没有已发布课程');
expect(screen.getByRole('dialog')).toHaveTextContent('今天想学会什么?');
expect(screen.getByLabelText('课程需求')).toBeEnabled();
expect(screen.getByRole('button', { name: '后台生成' })).toBeDisabled();
expect(screen.getByRole('button', { name: '联网搜索' })).toHaveAttribute('aria-pressed', 'false');
expect(screen.getByRole('button', { name: '语音讲解' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByRole('button', { name: '互动模式' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByRole('button', { name: '职教任务' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '语音输入' })).toBeInTheDocument();
expect(screen.queryByText(/供应商选择/)).not.toBeInTheDocument();
});
it('submits every canonical option explicitly to the background job', async () => {
fetchLearningCoursesMock.mockResolvedValue([]);
startLearningGenerationMock.mockResolvedValue({
contractVersion: 2,
jobId: 'job-1',
status: 'queued',
mode: 'single',
done: false,
});
fetchLearningGenerationMock.mockImplementation(() => new Promise(() => undefined));
render();
fireEvent.change(await screen.findByLabelText('课程需求'), { target: { value: '用互动练习教我二次函数' } });
fireEvent.click(screen.getByRole('button', { name: '联网搜索' }));
fireEvent.click(screen.getByRole('button', { name: '生成图片' }));
fireEvent.click(screen.getByRole('button', { name: '职教任务' }));
fireEvent.click(screen.getByRole('button', { name: '后台生成' }));
await waitFor(() => expect(startLearningGenerationMock).toHaveBeenCalledWith({
requirement: '用互动练习教我二次函数',
enableWebSearch: true,
enableImageGeneration: true,
enableVideoGeneration: false,
enableTTS: true,
interactiveMode: true,
taskEngineMode: true,
}, []));
});
it('renders the course plaza without adding another sidebar', async () => {
fetchLearningCoursesMock.mockResolvedValue([]);
render();
await screen.findByText('暂时还没有已发布课程');
expect(screen.getByRole('region', { name: '课程广场' })).toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.queryByRole('complementary')).not.toBeInTheDocument();
});
});