Files
makelore/tests/unit/coding-teacher-ui.test.tsx

450 lines
26 KiB
TypeScript

import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { TeacherChatPanel } from '@/pages/Chat/TeacherChatPanel';
import type { TeacherDefinition, TeacherRequest, TeacherSend, TeacherTopic } from '../../shared/coding-teacher';
const api = vi.hoisted(() => ({
config: vi.fn(),
preview: vi.fn(),
list: vi.fn(),
create: vi.fn(),
read: vi.fn(),
send: vi.fn(),
cancel: vi.fn(),
save: vi.fn(),
events: vi.fn(),
}));
vi.mock('@/lib/coding-teacher', () => ({
teacherApi: api,
teacherTopicsPath: (p: string, s: string, role?: string) => p + '/' + (role ?? s),
}));
const definition: TeacherDefinition = {
schema_version: 1,
teacher_id: 'coding-teacher',
name: '老师',
description: '',
avatar_id: 'avatar-01',
welcome_message: '欢迎',
suggested_questions: [],
system_prompt: '教学',
skills: [],
model: { model_id: 'qwen', reasoning_choice: { mode: 'default' } },
limits: { max_input_tokens: 8000, max_output_tokens: 1500 },
};
function topic(id: string): TeacherTopic {
return {
schemaVersion: 1,
revision: 1,
id,
accountId: 'user',
projectId: 'p',
sourceConversationId: 'c',
definition,
version: 1,
createdAt: '2026-09-22',
updatedAt: '2026-09-22',
requests: [],
};
}
const first = topic('first'),
second = topic('second');
function request(overrides: Partial<TeacherRequest> = {}): TeacherRequest {
return {
id: 'request-1', text: '老师,帮我看看', references: [], createdAt: 'now',
sourceCursor: { workerGeneration: 1, seq: 1 }, sourceCapturedAt: 'now',
includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: '',
...overrides,
};
}
const suggestedQuestions = ['怎样知道别人看懂了规则?', '我该先试哪个想法?'];
function suggestionsRequest(overrides: Partial<TeacherRequest> = {}): TeacherRequest {
return request({ intent: 'suggestions', response: '我们可以从最近遇到的这两个地方聊起。', suggestedQuestions, ...overrides });
}
let streams: Map<string, EventTarget & { close: ReturnType<typeof vi.fn> }>;
beforeEach(() => {
vi.resetAllMocks();
localStorage.clear();
streams = new Map();
api.config.mockResolvedValue({ enabled: true, published_version: 1, revision: 1, definition });
api.list.mockResolvedValue({
items: [
{ id: 'first', title: '第一个' },
{ id: 'second', title: '第二个' },
],
lastSelectedTopicId: 'first',
});
api.read.mockImplementation(async (_base, id) =>
id === 'first' ? structuredClone(first) : structuredClone(second)
);
api.create.mockResolvedValue(first);
api.events.mockImplementation(async (_base, id) => {
const stream = Object.assign(new EventTarget(), { close: vi.fn() });
streams.set(id, stream);
return stream;
});
});
async function ready() {
await waitFor(() =>
expect(screen.getByRole('combobox', { name: '老师话题' })).toHaveValue('first')
);
await waitFor(() => expect(streams.has('first')).toBe(true));
}
describe('teacher side chat', () => {
it('retains failed input and retries with the same request id, then appends a reply only on click', async () => {
const bringBack = vi.fn();
api.send
.mockRejectedValueOnce(new Error('网络暂不可用'))
.mockImplementationOnce(async (_base, _id, input) => ({
...first,
revision: 3,
requests: [
{
id: input.requestId,
text: input.text,
references: [],
createdAt: 'now',
sourceCursor: { workerGeneration: 1, seq: 1 },
sourceCapturedAt: 'now',
includedSourceMessageIds: [],
omittedMessages: 0,
status: 'completed',
response: '教学回答',
},
],
}));
render(<TeacherChatPanel projectId="p" sourceId="c" onBringBack={bringBack} />);
await ready();
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '解释代码' } });
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
await screen.findByText('网络暂不可用');
expect(screen.getByLabelText('向老师提问')).toHaveValue('解释代码');
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
await screen.findByText('教学回答');
expect(api.send.mock.calls[0][2].requestId).toBe(api.send.mock.calls[1][2].requestId);
expect(bringBack).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '带回主会话草稿' }));
expect(bringBack).toHaveBeenCalledWith('教学回答');
});
it('ignores an old topic stream after switching and does not cancel when closed', async () => {
const closed = vi.fn();
const view = render(<TeacherChatPanel projectId="p" sourceId="c" onClose={closed} />);
await ready();
const old = streams.get('first')!;
fireEvent.change(screen.getByRole('combobox', { name: '老师话题' }), {
target: { value: 'second' },
});
await waitFor(() =>
expect(screen.getByRole('combobox', { name: '老师话题' })).toHaveValue('second')
);
act(() =>
old.dispatchEvent(
new MessageEvent('snapshot', { data: JSON.stringify({ ...first, revision: 999 }) })
)
);
expect(screen.getByRole('combobox', { name: '老师话题' })).toHaveValue('second');
fireEvent.click(screen.getByRole('button', { name: '关闭老师' }));
expect(closed).toHaveBeenCalledOnce();
view.unmount();
expect(api.cancel).not.toHaveBeenCalled();
expect(old.close).toHaveBeenCalled();
});
it('shows disabled history while blocking a new question', async () => {
api.config.mockResolvedValue({ enabled: false, definition });
render(<TeacherChatPanel projectId="p" sourceId="c" />);
await ready();
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '问题' } });
expect(screen.getByRole('button', { name: '提问', exact: true })).toBeDisabled();
expect(api.read).toHaveBeenCalled();
});
it('keeps friend suggestions as drafts until the student submits them', async () => {
const role = 'friend';
const label = '朋友';
const suggestion = '想听听你对作品的第一印象';
const sendLabel = '发送给朋友';
api.list.mockResolvedValue({ items: [], lastSelectedTopicId: null });
api.send.mockImplementation(async (_base, _id, input) => ({ ...first, requests: [{
id: input.requestId, text: input.text, references: [], createdAt: 'now',
sourceCursor: { workerGeneration: 1, seq: 1 }, sourceCapturedAt: 'now',
includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: '一起再想一想。',
}] }));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" role={role} />);
const sendButton = screen.getByRole('button', { name: sendLabel, exact: true });
await waitFor(() => expect(screen.getByLabelText(`向${label}提问`)).toBeEnabled());
fireEvent.click(screen.getByRole('button', { name: suggestion }));
expect(screen.getByLabelText(`向${label}提问`)).toHaveValue(suggestion);
expect(api.create).not.toHaveBeenCalled();
expect(api.send).not.toHaveBeenCalled();
fireEvent.click(sendButton);
await screen.findByText('一起再想一想。');
expect(api.config).toHaveBeenCalledWith(role);
expect(api.send).toHaveBeenCalledWith(`p/${role}`, 'first', expect.objectContaining({
text: suggestion, sourceConversationId: 'c', references: [],
}));
expect(screen.getByLabelText(`向${label}提问`)).toHaveValue('');
view.unmount();
render(<TeacherChatPanel projectId="p" sourceId="c" role={role} />);
expect(screen.getByLabelText(`向${label}提问`)).toHaveValue('');
});
it.each(['teacher', 'friend'] as const)('opens %s without an extra project row or a model request', async (role) => {
api.list.mockResolvedValue({ items: [], lastSelectedTopicId: null });
api.config.mockResolvedValue({ enabled: true, published_version: 1, revision: 1, definition: { ...definition, suggested_questions: ['固定的通用问题'] } });
render(<TeacherChatPanel projectId="p" projectName="星星收集游戏" sourceId="c" role={role} />);
await waitFor(() => expect(screen.getByRole('textbox')).toBeEnabled());
expect(screen.queryByText('星星收集游戏')).not.toBeInTheDocument();
expect(screen.queryByText('当前项目')).not.toBeInTheDocument();
expect(api.create).not.toHaveBeenCalled();
expect(api.send).not.toHaveBeenCalled();
if (role === 'teacher') {
expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled();
expect(screen.queryByRole('button', { name: '固定的通用问题' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '我还没想好下一步做什么' })).not.toBeInTheDocument();
} else {
expect(screen.getByRole('button', { name: '固定的通用问题' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: '老师,帮我看看', exact: true })).not.toBeInTheDocument();
}
});
it('requests contextual suggestions explicitly, hides streaming JSON and preserves the freeform draft', async () => {
const streamingJson = '{"intro":"还没输出完","questions":["尚未确认的问题';
api.send.mockImplementation(async (_base, _id, input: TeacherSend) => ({
...first, revision: 2, requests: [suggestionsRequest({ id: input.requestId, status: 'running', response: streamingJson, suggestedQuestions: undefined })],
}));
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
const help = screen.getByRole('button', { name: '老师,帮我看看', exact: true });
await waitFor(() => expect(help).toBeEnabled());
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我自己还想问的另一件事' } });
fireEvent.click(help);
await screen.findByText('我看看你最近做到了哪里…');
expect(screen.queryByText(streamingJson)).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '尚未确认的问题' })).not.toBeInTheDocument();
expect(api.send).toHaveBeenCalledWith('p/teacher', 'first', expect.objectContaining({
intent: 'suggestions', text: '老师,帮我看看', sourceConversationId: 'c',
}));
expect(screen.getByLabelText('向老师提问')).toHaveValue('我自己还想问的另一件事');
expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeDisabled();
await waitFor(() => expect(streams.has('first')).toBe(true));
const sent = api.send.mock.calls[0][2] as TeacherSend;
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify({
...first, revision: 3, requests: [suggestionsRequest({ id: sent.requestId })],
}) })));
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
expect(screen.queryByText('我看看你最近做到了哪里…')).not.toBeInTheDocument();
for (const question of suggestedQuestions) expect(screen.getByRole('button', { name: question })).toBeEnabled();
expect(screen.getByRole('button', { name: '我也说不清,你带我看看', exact: true })).toBeEnabled();
expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled();
expect(api.send).toHaveBeenCalledTimes(1);
expect(screen.getByLabelText('向老师提问')).toHaveValue('我自己还想问的另一件事');
});
it.each([
[suggestedQuestions[0], undefined],
['我也说不清,你带我看看', 'guided-help'],
] as const)('sends the selected help action "%s" without replacing the student draft', async (text, intent) => {
const previous = suggestionsRequest();
api.read.mockResolvedValue({ ...first, requests: [previous] });
api.send.mockImplementation(async (_base, _id, input: TeacherSend) => ({
...first, revision: 2, requests: [previous, request({ id: input.requestId, text: input.text, intent: input.intent, response: '我们先从你刚才做的那一步看起。' })],
}));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" quote={{ kind: 'code', text: '学生正在引用的一段代码' }} />);
await waitFor(() => expect(screen.getByRole('button', { name: text, exact: true })).toBeEnabled());
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '还在整理的自由提问' } });
expect(api.send).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: text, exact: true }));
await screen.findByText('我们先从你刚才做的那一步看起。');
expect(api.send).toHaveBeenCalledTimes(1);
const sent = api.send.mock.calls[0][2] as TeacherSend;
expect(sent).toMatchObject({ text, sourceConversationId: 'c' });
expect(sent.references).toEqual([]);
expect(sent.intent).toBe(intent);
expect(screen.getByLabelText('向老师提问')).toHaveValue('还在整理的自由提问');
expect(screen.getByText('学生正在引用的一段代码')).toBeInTheDocument();
view.unmount();
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
expect(screen.getByLabelText('向老师提问')).toHaveValue('还在整理的自由提问');
expect(screen.getByText('学生正在引用的一段代码')).toBeInTheDocument();
expect(api.send).toHaveBeenCalledTimes(1);
});
it('retries an uncertain suggestions request after reopening without losing the student draft', async () => {
api.send.mockRejectedValue(new Error('网络暂不可用'));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled());
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我还没写完的问题' } });
fireEvent.click(screen.getByRole('button', { name: '老师,帮我看看', exact: true }));
await screen.findByText('网络暂不可用');
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
expect(originalRequest.intent).toBe('suggestions');
view.unmount();
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled());
expect(screen.getByLabelText('向老师提问')).toHaveValue('我还没写完的问题');
expect(api.send).toHaveBeenCalledTimes(1);
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我又补了一点自己的想法' } });
fireEvent.click(screen.getByRole('button', { name: '老师,帮我看看', exact: true }));
await screen.findByText('网络暂不可用');
expect(api.send.mock.calls[1][2]).toEqual(originalRequest);
expect(screen.getByLabelText('向老师提问')).toHaveValue('我又补了一点自己的想法');
});
it('shows a failed suggestion request without leaking raw JSON and starts a fresh request on another try', async () => {
const failed = suggestionsRequest({ status: 'failed', response: '{"intro":"未完成', suggestedQuestions: undefined, error: '这次没有看清楚,请再试一次。' });
api.read.mockResolvedValue({ ...first, requests: [failed] });
api.send.mockImplementation(async (_base, _id, input: TeacherSend) => ({
...first, revision: 2, requests: [failed, suggestionsRequest({ id: input.requestId })],
}));
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await screen.findByText('这次没有看清楚,请再试一次。');
expect(screen.queryByText('{"intro":"未完成')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: suggestedQuestions[0] })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '再请老师看看', exact: true }));
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
expect(api.send.mock.calls[0][2]).toMatchObject({ intent: 'suggestions' });
expect(api.send.mock.calls[0][2].requestId).not.toBe(failed.id);
});
it.each(['reopen', 'stream'] as const)('reconciles an accepted request learned through %s before retrying a failed reply', async (via) => {
let accepted: TeacherRequest;
api.send.mockRejectedValueOnce(new Error('响应中途断开')).mockImplementation(async (_base, _id, input: TeacherSend) => ({
...first, revision: 3, requests: [accepted, suggestionsRequest({ id: input.requestId })],
}));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled());
await waitFor(() => expect(streams.has('first')).toBe(true));
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我的自由提问继续留着' } });
fireEvent.click(screen.getByRole('button', { name: '老师,帮我看看', exact: true }));
await screen.findByText('响应中途断开');
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
accepted = suggestionsRequest({ id: originalRequest.requestId, status: 'failed', response: '', suggestedQuestions: undefined, error: '老师已接到,但这次回复失败了' });
const recovered = { ...first, revision: 2, requests: [accepted] };
if (via === 'reopen') {
view.unmount();
api.read.mockResolvedValue(recovered);
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
} else {
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify(recovered) })));
}
await screen.findByText('老师已接到,但这次回复失败了');
await waitFor(() => expect(screen.getByRole('button', { name: '再请老师看看', exact: true })).toBeEnabled());
expect(screen.getByLabelText('向老师提问')).toHaveValue('我的自由提问继续留着');
expect(api.send).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole('button', { name: '再请老师看看', exact: true }));
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
expect(api.send).toHaveBeenCalledTimes(2);
expect(api.send.mock.calls[1][2].requestId).not.toBe(originalRequest.requestId);
expect(screen.getByLabelText('向老师提问')).toHaveValue('我的自由提问继续留着');
});
it('still submits a student-written question with its quote and clears only that submitted draft', async () => {
const quote = { kind: 'code' as const, text: 'setScore(score + 1)' };
api.send.mockImplementation(async (_base, _id, input: TeacherSend) => ({
...first, revision: 2, requests: [request({
id: input.requestId, text: input.text, references: input.references ?? [], response: '你觉得每点一次,分数应该怎么变化?',
})],
}));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" quote={quote} />);
await waitFor(() => expect(screen.getByLabelText('向老师提问')).toBeEnabled());
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '为什么分数没有增加?' } });
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
await screen.findByText('你觉得每点一次,分数应该怎么变化?');
expect(api.send.mock.calls[0][2]).toMatchObject({ text: '为什么分数没有增加?', references: [quote], sourceConversationId: 'c' });
expect(api.send.mock.calls[0][2].intent).toBeUndefined();
expect(screen.getByLabelText('向老师提问')).toHaveValue('');
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '老师,帮我看看', exact: true })).toBeEnabled();
view.unmount();
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
expect(screen.getByLabelText('向老师提问')).toHaveValue('');
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
expect(api.send).toHaveBeenCalledTimes(1);
});
it('restores a closed draft and keeps teacher, friend and project drafts separate', async () => {
api.list.mockResolvedValue({ items: [], lastSelectedTopicId: null });
const closed = vi.fn();
const view = render(<TeacherChatPanel key="p-teacher" projectId="p" sourceId="c" role="teacher" onClose={closed} />);
await waitFor(() => expect(screen.getByLabelText('向老师提问')).toBeEnabled());
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我还没想清楚规则' } });
fireEvent.click(screen.getByRole('button', { name: '关闭老师' }));
expect(closed).toHaveBeenCalledOnce();
view.rerender(<></>);
view.rerender(<TeacherChatPanel key="p-teacher" projectId="p" sourceId="another-operation-chat" role="teacher" />);
expect(screen.getByLabelText('向老师提问')).toHaveValue('我还没想清楚规则');
view.rerender(<TeacherChatPanel key="p-friend" projectId="p" sourceId="c" role="friend" />);
await waitFor(() => expect(screen.getByLabelText('向朋友提问')).toBeEnabled());
expect(screen.getByLabelText('向朋友提问')).toHaveValue('');
fireEvent.change(screen.getByLabelText('向朋友提问'), { target: { value: '想听听你的感受' } });
view.rerender(<TeacherChatPanel key="another-teacher" projectId="another-project" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByLabelText('向老师提问')).toBeEnabled());
expect(screen.getByLabelText('向老师提问')).toHaveValue('');
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '这个项目的另一个问题' } });
view.rerender(<TeacherChatPanel key="p-teacher" projectId="p" sourceId="c" role="teacher" />);
expect(screen.getByLabelText('向老师提问')).toHaveValue('我还没想清楚规则');
view.rerender(<TeacherChatPanel key="p-friend" projectId="p" sourceId="c" role="friend" />);
expect(screen.getByLabelText('向朋友提问')).toHaveValue('想听听你的感受');
expect(api.send).not.toHaveBeenCalled();
expect(api.cancel).not.toHaveBeenCalled();
});
it('keeps a failed question and its request id when the consultation is reopened', async () => {
api.send.mockRejectedValue(new Error('网络暂不可用'));
const view = render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByLabelText('向老师提问')).toBeEnabled());
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我该怎样观察?' } });
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
await screen.findByText('网络暂不可用');
const originalRequest = api.send.mock.calls[0][2];
view.unmount();
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByRole('button', { name: '提问', exact: true })).toBeEnabled());
expect(screen.getByLabelText('向老师提问')).toHaveValue('我该怎样观察?');
expect(api.send).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
await screen.findByText('网络暂不可用');
expect(api.send.mock.calls[1][2]).toEqual(originalRequest);
});
it('returns to the operation chat without importing advice or offering a notes workflow', async () => {
const returnToWork = vi.fn();
const bringBack = vi.fn();
api.read.mockResolvedValue({ ...first, requests: [{
id: 'reply', text: '游戏不好玩', references: [], createdAt: 'now',
sourceCursor: { workerGeneration: 1, seq: 1 }, sourceCapturedAt: 'now',
includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: '先观察别人在哪儿停下来。',
}] });
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" onReturnToWork={returnToWork} onBringBack={bringBack} />);
await screen.findByText('先观察别人在哪儿停下来。');
expect(screen.queryByRole('button', { name: '带回主会话草稿' })).not.toBeInTheDocument();
expect(screen.queryByText(/记一下|项目共识|记入共识|待办/)).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '我去试一试' }));
expect(returnToWork).toHaveBeenCalledOnce();
expect(bringBack).not.toHaveBeenCalled();
expect(api.send).not.toHaveBeenCalled();
});
it('keeps operations preview available without a consultation role', async () => {
api.preview.mockResolvedValue({ payload: { ...definition, suggested_questions: ['运营配置的预览问题'] }, draft_revision: 7 });
api.list.mockResolvedValue({ items: [], lastSelectedTopicId: null });
api.create.mockResolvedValue({ ...first, draftRevision: 7 });
render(<TeacherChatPanel projectId="preview" sourceId="sample" draftRevision={7} sampleContext="学生的练习代码" />);
await waitFor(() => expect(screen.getByRole('button', { name: '老师新话题' })).toBeEnabled());
expect(screen.getByText('运营草稿试聊')).toBeInTheDocument();
expect(api.preview).toHaveBeenCalledWith(7);
expect(api.config).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '运营配置的预览问题' }));
expect(screen.getByLabelText('向老师提问')).toHaveValue('运营配置的预览问题');
expect(api.send).not.toHaveBeenCalled();
expect(screen.queryByRole('button', { name: '老师,帮我看看', exact: true })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '老师新话题' }));
await waitFor(() => expect(api.create).toHaveBeenCalledWith('preview/sample', 7, '学生的练习代码'));
});
});