feat: integrate pixel teacher presence and resilient classroom preview

This commit is contained in:
鲨鱼辣椒
2026-09-23 18:23:53 +08:00
parent 12d800ec51
commit 7951cca700
60 changed files with 3313 additions and 180 deletions

View File

@@ -2,6 +2,7 @@ import { act, fireEvent, render, screen, waitFor, within } from '@testing-librar
import { Profiler } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createProductSnapshot } from '../fixtures/coding-conversation-product-fixtures';
import { OPEN_WORK_PROMPT } from '../../shared/coding-work-preview';
const attachmentApi = vi.hoisted(() => ({ load: vi.fn() }));
@@ -10,8 +11,61 @@ vi.mock('@/lib/coding-attachments', () => ({
}));
describe('CodingConversationTimeline', () => {
it('gives the failed hidden work action one login recovery and removes it when work is ready', async () => {
const { codingConversationStore } = await import('@/stores/coding-conversations');
const { CodingConversationTimeline } = await import('@/pages/Chat/CodingConversationTimeline');
const snapshot = createProductSnapshot('failed-work-recovery', 1);
snapshot.run = { status: 'idle' };
snapshot.nodes = [
{ kind: 'message', id: 'work', role: 'user', status: 'complete', blocks: [{ kind: 'text', id: 'prompt', status: 'complete', text: OPEN_WORK_PROMPT }] },
{ kind: 'message', id: 'failure', role: 'assistant', status: 'error', stopReason: 'error', blocks: [] },
{ kind: 'notice', id: 'auth', level: 'error', code: 'CODING_PROVIDER_AUTH_REQUIRED', message: '登录已失效,请重新登录后继续。' },
];
codingConversationStore.getState().primeConversation(snapshot);
const onRecovery = vi.fn();
const view = render(<CodingConversationTimeline conversationId="failed-work-recovery" onRecovery={onRecovery} />);
expect(screen.queryByText('本次处理失败。')).not.toBeInTheDocument();
expect(screen.getAllByTestId('coding-recovery-card')).toHaveLength(1);
fireEvent.click(screen.getByRole('button', { name: '登录并继续' }));
await waitFor(() => expect(onRecovery).toHaveBeenCalledWith('login', true));
view.rerender(<CodingConversationTimeline conversationId="failed-work-recovery" onRecovery={onRecovery} workReady />);
expect(screen.queryByTestId('coding-recovery-card')).not.toBeInTheDocument();
expect(screen.queryByTestId('coding-process-group')).not.toBeInTheDocument();
});
it('does not offer to replay a request whose result is uncertain', async () => {
const { codingConversationStore } = await import('@/stores/coding-conversations');
const { CodingConversationTimeline } = await import('@/pages/Chat/CodingConversationTimeline');
const snapshot = createProductSnapshot('uncertain-recovery', 1);
snapshot.run = { status: 'error', error: { code: 'CODING_REQUEST_UNCERTAIN', message: '等待确认', recoverable: true } };
snapshot.nodes = [{ kind: 'message', id: 'failure', role: 'assistant', status: 'error', blocks: [] }];
codingConversationStore.getState().primeConversation(snapshot);
const onRecovery = vi.fn();
render(<CodingConversationTimeline conversationId="uncertain-recovery" onRecovery={onRecovery} />);
fireEvent.click(screen.getByRole('button', { name: '查看最新进展' }));
await waitFor(() => expect(onRecovery).toHaveBeenCalledWith('refresh', false));
});
afterEach(() => vi.unstubAllGlobals());
it('hides the internal work action without displaying a student message', async () => {
const { codingConversationStore, selectCodingConversationSnapshot } = await import('@/stores/coding-conversations');
const { CodingConversationTimeline } = await import('@/pages/Chat/CodingConversationTimeline');
const snapshot = createProductSnapshot('work-action', 1);
snapshot.nodes = [
{ kind: 'message', id: 'open-work', role: 'user', status: 'complete', sourceEntryId: 'open-work', blocks: [{ kind: 'text', id: 'work-text', status: 'complete', text: OPEN_WORK_PROMPT }] },
{ kind: 'message', id: 'work-result', role: 'assistant', status: 'complete', blocks: [{ kind: 'text', id: 'result-text', status: 'complete', text: '作品已经打开了。' }] },
];
codingConversationStore.getState().primeConversation(snapshot);
render(<CodingConversationTimeline conversationId="work-action" onFork={vi.fn()} />);
expect(screen.queryByLabelText('系统操作')).not.toBeInTheDocument();
expect(screen.queryByText('已点击打开作品')).not.toBeInTheDocument();
expect(screen.queryByLabelText('你的消息')).not.toBeInTheDocument();
expect(screen.queryByText(/学生点击了/)).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '从这里创建新对话分支' })).not.toBeInTheDocument();
expect(screen.getByText('作品已经打开了。')).toBeInTheDocument();
expect(selectCodingConversationSnapshot('work-action')(codingConversationStore.getState())?.nodes).toEqual(snapshot.nodes);
});
it.each([
['makelore.game-resource', '游戏资源'],
['makelore.game-audio', '游戏音频'],
@@ -753,7 +807,7 @@ describe('CodingConversationTimeline', () => {
render(<CodingConversationTimeline conversationId="conversation-failed-process" />);
expect(within(screen.getByTestId('coding-process-group')).getByText('处理失败 · 5 秒'))
expect(within(screen.getByTestId('coding-process-group')).getByText('这一步还没有完成 · 5 秒'))
.toBeVisible();
});