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(); }); });