feat: integrate learning module
This commit is contained in:
@@ -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']}>
|
||||
|
||||
Reference in New Issue
Block a user