feat: integrate learning module
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-16 20:52:16 +08:00
parent 26b52d76e3
commit 01bee3188b
107 changed files with 6318 additions and 12063 deletions

View File

@@ -1,10 +1,19 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it } from 'vitest';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
import { ModuleSwitcher } from '@/components/layout/ModuleSwitcher';
import { ModuleSelection } from '@/pages/ModuleSelection';
import { useAuthStore } from '@/stores/auth';
import { useOpencodeStore } from '@/stores/opencode';
import { useUserProfileStore } from '@/stores/user-profile';
const { hostApiFetchMock } = vi.hoisted(() => ({
hostApiFetchMock: vi.fn(),
}));
vi.mock('@/lib/host-api', () => ({
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
}));
function LocationProbe() {
const location = useLocation();
@@ -13,6 +22,7 @@ function LocationProbe() {
describe('AI module navigation', () => {
beforeEach(() => {
vi.clearAllMocks();
useAuthStore.setState({
initialized: true,
loading: false,
@@ -25,6 +35,21 @@ describe('AI module navigation', () => {
user: { username: 'zhangsan@example.com', userId: 'user-1', tenantId: null, deptId: null, authorities: [] },
});
useOpencodeStore.setState({ activeProject: null });
useUserProfileStore.setState({ profilesByUserId: {}, syncStatesByUserId: {} });
hostApiFetchMock.mockResolvedValue({
success: true,
profile: {
display_name: '小明',
age: null,
gender: null,
share_age_with_agents: false,
share_gender_with_agents: false,
analysis_enabled: true,
completed: true,
version: 1,
updated_at: '2026-08-16T00:00:00Z',
},
});
});
it('shows the four module choices and routes Canvas from the chooser', async () => {
@@ -41,7 +66,7 @@ describe('AI module navigation', () => {
expect(screen.getByRole('heading', { name: '今天,想创作什么?' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '今天,想创作什么?' })).toHaveClass('whitespace-nowrap');
expect(screen.getByTestId('ai-module-options')).toHaveClass('module-selection-options');
expect(screen.getByText('zhangsan,欢迎回来!')).toBeInTheDocument();
expect(await screen.findByText('小明,欢迎回来!')).toBeInTheDocument();
expect(screen.getByTestId('ai-module-option-programming')).toHaveClass('module-option-card', 'module-option-card-horizontal');
for (const moduleId of ['programming', 'painting', 'learning', 'robot'] as const) {
expect(screen.getByTestId(`ai-module-option-${moduleId}`).querySelector('.module-option-artwork')).toBeNull();
@@ -71,9 +96,7 @@ describe('AI module navigation', () => {
expect(screen.getByAltText('青少年管理机器人硬件与设备绑定')).toBeInTheDocument();
fireEvent.click(learningOption);
expect(screen.getByTestId('ai-module-selection-page')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('ai-module-option-painting'));
expect(await screen.findByTestId('location-path')).toHaveTextContent('/image-canvas');
expect(await screen.findByTestId('location-path')).toHaveTextContent('/learning');
});
it('routes Robot from the chooser to the AI hardware workspace', async () => {
@@ -148,6 +171,58 @@ describe('AI module navigation', () => {
expect(await screen.findByTestId('location-path')).toHaveTextContent('/module-select');
});
it('requires a personal profile before the first module can be used', async () => {
hostApiFetchMock.mockImplementation(async (_path: string, init?: RequestInit) => {
if (init?.method === 'PUT') {
return {
success: true,
profile: {
display_name: '小红',
age: null,
gender: null,
share_age_with_agents: false,
share_gender_with_agents: false,
analysis_enabled: true,
completed: true,
version: 1,
updated_at: '2026-08-16T00:00:00Z',
},
};
}
return {
success: true,
profile: {
display_name: null,
age: null,
gender: null,
share_age_with_agents: false,
share_gender_with_agents: false,
analysis_enabled: true,
completed: false,
version: 0,
updated_at: null,
},
};
});
render(
<MemoryRouter initialEntries={['/module-select']}>
<Routes>
<Route path="/module-select" element={<ModuleSelection />} />
<Route path="*" element={<LocationProbe />} />
</Routes>
</MemoryRouter>,
);
expect(await screen.findByText('首次使用前请先填写名字。个人资料与登录账号信息相互独立。')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: '关闭个人资料' })).not.toBeInTheDocument();
fireEvent.change(screen.getByLabelText(/名字/), { target: { value: '小红' } });
fireEvent.click(screen.getByRole('button', { name: '保存个人资料' }));
await waitFor(() => expect(screen.queryByRole('dialog', { name: '个人资料' })).not.toBeInTheDocument());
expect(screen.getByText('小红,欢迎回来!')).toBeInTheDocument();
});
it('shows Robot as the active module and returns to the chooser from the AI hardware route', async () => {
render(
<MemoryRouter initialEntries={['/ai-hardware']}>
@@ -162,6 +237,20 @@ describe('AI module navigation', () => {
expect(await screen.findByTestId('location-path')).toHaveTextContent('/module-select');
});
it('shows Learning as the active module and returns to the chooser from the learning route', async () => {
render(
<MemoryRouter initialEntries={['/learning']}>
<Routes>
<Route path="*" element={<><ModuleSwitcher sidebarCollapsed={false} /><LocationProbe /></>} />
</Routes>
</MemoryRouter>,
);
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveTextContent('学习 Learning');
fireEvent.click(screen.getByTestId('sidebar-module-switcher-trigger'));
expect(await screen.findByTestId('location-path')).toHaveTextContent('/module-select');
});
it('keeps breathing room around the compact module switcher trigger', () => {
render(
<MemoryRouter initialEntries={['/opencode-chat']}>