feat(learning): replace courses with project catalog

This commit is contained in:
2026-08-20 00:08:04 +08:00
parent 2cb8a7aef4
commit 38db1589e2
56 changed files with 1741 additions and 8725 deletions

View File

@@ -1,68 +1,33 @@
import { fireEvent, render, screen, within } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
import { LearningSidebar } from '@/components/layout/LearningSidebar';
const fetchMyLearningCoursesMock = vi.hoisted(() => vi.fn());
const listInstalledLearningCoursesMock = vi.hoisted(() => vi.fn());
const downloadLearningCourseMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/learning', () => ({
LEARNING_LIBRARY_CHANGED_EVENT: 'makelore:learning-library-changed',
fetchMyLearningCourses: fetchMyLearningCoursesMock,
listInstalledLearningCourses: listInstalledLearningCoursesMock,
downloadLearningCourse: downloadLearningCourseMock,
}));
const downloadedCourse = {
id: 'course-local',
origin: 'ops_large',
status: 'published',
title: '本机互动课',
summary: null,
language: 'zh-CN',
contentHash: 'c'.repeat(64),
archiveSha256: 'd'.repeat(64),
archiveBytes: 2048,
formatVersion: 1,
minPlayerVersion: '1.0.0',
sceneCount: 8,
capabilities: { sceneKinds: ['slide'], hasAudio: false, hasWhiteboard: false, hasAgent: true },
createdAt: '2026-08-16T00:00:00Z',
publishedAt: '2026-08-16T00:00:00Z',
} as const;
function LocationProbe() {
const location = useLocation();
return <output data-testid="location-path">{location.pathname}{location.search}</output>;
return <output data-testid="location-path">{location.pathname}</output>;
}
describe('LearningSidebar', () => {
beforeEach(() => {
fetchMyLearningCoursesMock.mockReset();
listInstalledLearningCoursesMock.mockReset();
downloadLearningCourseMock.mockReset();
fetchMyLearningCoursesMock.mockResolvedValue([{ ...downloadedCourse, id: 'course-generated', origin: 'user_single', status: 'ready', title: '我生成的物理课', publishedAt: null }]);
listInstalledLearningCoursesMock.mockResolvedValue([{ schemaVersion: 1, course: downloadedCourse, archivePath: '/tmp/course-local.zip', installedAt: '2026-08-16T00:00:00Z' }]);
});
it('keeps generation and personal course cards in the existing main sidebar', async () => {
it('shows only the new project catalog navigation and returns detail pages to the list', () => {
render(
<MemoryRouter initialEntries={['/learning']}>
<MemoryRouter initialEntries={['/learning/project/project-1']}>
<Routes>
<Route path="*" element={<><LearningSidebar sidebarCollapsed={false} /><LocationProbe /></>} />
</Routes>
</MemoryRouter>,
);
const sidebarContent = screen.getByTestId('sidebar-learning-navigation');
expect(within(sidebarContent).getByRole('button', { name: '生成课程' })).toBeEnabled();
expect(await within(sidebarContent).findByText('本机互动课')).toBeInTheDocument();
expect(within(sidebarContent).getByText('我生成的物理课')).toBeInTheDocument();
expect(within(sidebarContent).getByText('已下载到本机')).toBeInTheDocument();
expect(within(sidebarContent).getByText('我生成的,待下载')).toBeInTheDocument();
expect(screen.getByRole('navigation', { name: 'AI 学习导航' })).toHaveTextContent('学习项目');
expect(screen.queryByText('生成课程')).not.toBeInTheDocument();
expect(screen.queryByText('我的课程')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '学习项目' }));
expect(screen.getByTestId('location-path')).toHaveTextContent('/learning');
});
fireEvent.click(within(sidebarContent).getByRole('button', { name: '生成课程' }));
expect(screen.getByTestId('location-path')).toHaveTextContent('/learning?generate=1');
it('keeps the collapsed navigation compact', () => {
render(<MemoryRouter><LearningSidebar sidebarCollapsed /></MemoryRouter>);
expect(screen.getByRole('button', { name: '学习项目' })).toBeInTheDocument();
expect(screen.queryByText('阅读项目说明并下载到电脑实践。')).not.toBeInTheDocument();
});
});