Integrate project classroom with teacher and friend consultations

This commit is contained in:
鲨鱼辣椒
2026-09-22 15:50:18 +08:00
parent e5d271bc45
commit 68e676cb62
28 changed files with 957 additions and 314 deletions

View File

@@ -205,6 +205,70 @@ describe('CodingChatPanel first Conversation', () => {
vi.resetModules();
});
it('keeps the operation draft when switching work tabs and reopening separate teacher and friend drafts', async () => {
localStorage.clear();
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config });
projectApi.conversations.mockResolvedValue([conversation]);
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
const { teacherApi } = await import('@/lib/coding-teacher');
vi.spyOn(teacherApi, 'config').mockResolvedValue({ enabled: true, published_version: 1, revision: 1, definition: null });
vi.spyOn(teacherApi, 'list').mockResolvedValue({ items: [], lastSelectedTopicId: null });
const consultationSend = vi.spyOn(teacherApi, 'send');
const consultationCreate = vi.spyOn(teacherApi, 'create');
const browserApi = await import('@/lib/agent-browser');
const closedBrowser = {
browserId: null, projectId: project.id, projectPath: null, state: 'closed' as const,
generation: 0, url: '', title: '', visible: false, bounds: null,
canGoBack: false, canGoForward: false, eventCursor: 0,
};
vi.spyOn(browserApi, 'getAgentBrowserState').mockResolvedValue(closedBrowser);
vi.spyOn(browserApi, 'closeAgentBrowser').mockResolvedValue(closedBrowser);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
render(<CodingChatPanel />);
const composer = await screen.findByTestId('coding-message-composer');
const operationInput = within(composer).getByRole('textbox');
fireEvent.change(operationInput, { target: { value: '我想先自己试一试' } });
const chatTab = screen.getByRole('tab', { name: '操作对话' });
const workTab = screen.getByRole('tab', { name: '作品', exact: true });
expect(chatTab).toHaveAttribute('aria-selected', 'true');
fireEvent.keyDown(chatTab, { key: 'ArrowRight' });
expect(workTab).toHaveFocus();
expect(workTab).toHaveAttribute('aria-selected', 'true');
expect(screen.getByRole('tabpanel', { name: '作品', exact: true })).toBeVisible();
expect(operationInput).not.toBeVisible();
fireEvent.keyDown(workTab, { key: 'ArrowLeft' });
expect(chatTab).toHaveFocus();
expect(operationInput).toBeVisible();
expect(operationInput).toHaveValue('我想先自己试一试');
fireEvent.click(screen.getByRole('button', { name: '老师', exact: true }));
const teacherInput = await screen.findByRole('textbox', { name: '向老师提问' });
await waitFor(() => expect(teacherInput).toBeEnabled());
fireEvent.change(teacherInput, { target: { value: '怎样判断规则清不清楚?' } });
fireEvent.click(screen.getByRole('button', { name: '朋友', exact: true }));
const friendInput = await screen.findByRole('textbox', { name: '向朋友提问' });
await waitFor(() => expect(friendInput).toBeEnabled());
expect(friendInput).toHaveValue('');
fireEvent.change(friendInput, { target: { value: '第一次玩的感受是什么?' } });
fireEvent.click(screen.getByRole('button', { name: '关闭朋友' }));
expect(screen.queryByTestId('teacher-chat-panel')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '朋友', exact: true }));
expect(screen.getByRole('textbox', { name: '向朋友提问' })).toHaveValue('第一次玩的感受是什么?');
fireEvent.click(screen.getByRole('button', { name: '老师', exact: true }));
expect(screen.getByRole('textbox', { name: '向老师提问' })).toHaveValue('怎样判断规则清不清楚?');
expect(screen.getByRole('button', { name: '老师', exact: true })).toHaveAttribute('aria-expanded', 'true');
expect(screen.getByRole('button', { name: '朋友', exact: true })).toHaveAttribute('aria-expanded', 'false');
expect(operationInput).toHaveValue('我想先自己试一试');
expect(screen.queryByText(/记一下|项目共识|记入共识/)).not.toBeInTheDocument();
expect(consultationCreate).not.toHaveBeenCalled();
expect(consultationSend).not.toHaveBeenCalled();
expect(conversationApi.submit).not.toHaveBeenCalled();
});
it('archives the last conversation without creating another, then restores the same running conversation', async () => {
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config });