Merge reply recovery and simplified consultation UI into ongoing chats
This commit is contained in:
@@ -10,7 +10,6 @@ const api = vi.hoisted(() => ({
|
||||
create: vi.fn(),
|
||||
read: vi.fn(),
|
||||
send: vi.fn(),
|
||||
updateDiscussion: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
save: vi.fn(),
|
||||
events: vi.fn(),
|
||||
@@ -55,7 +54,7 @@ const first = topic('first'),
|
||||
second = topic('second');
|
||||
function request(overrides: Partial<TeacherRequest> = {}): TeacherRequest {
|
||||
return {
|
||||
id: 'request-1', text: '帮我看看', references: [], createdAt: 'now',
|
||||
id: 'request-1', text: '给我一点提示', references: [], createdAt: 'now',
|
||||
sourceCursor: { workerGeneration: 1, seq: 1 }, sourceCapturedAt: 'now',
|
||||
includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: '',
|
||||
...overrides,
|
||||
@@ -162,6 +161,122 @@ describe('teacher side chat', () => {
|
||||
expect(details?.querySelector('script')).toBeNull();
|
||||
});
|
||||
|
||||
it('marks an incomplete completed reply instead of displaying a half sentence as a finished answer', async () => {
|
||||
const raw = '{"reply":"它早就不是"刚搭好架子"那种阶段了——还有后面的回答';
|
||||
const bringBack = vi.fn();
|
||||
api.read.mockResolvedValue({ ...first, requests: [request({
|
||||
text: '现在项目是什么阶段?', response: '它早就不是', replyIncomplete: true,
|
||||
replyParseError: 'format error', unparsedResponse: raw, suggestedQuestions: ['尚未确认的建议'],
|
||||
})] });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" onBringBack={bringBack} />);
|
||||
await ready();
|
||||
expect(screen.getByRole('status')).toHaveTextContent('这次回复未能完整显示');
|
||||
expect(screen.queryByTestId('teacher-reply')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '尚未确认的建议' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '带回主会话草稿' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '重新回答' })).toBeEnabled();
|
||||
const details = screen.getByText('查看收到的原始内容').closest('details');
|
||||
expect(details).not.toHaveAttribute('open');
|
||||
expect(details?.querySelector('pre')?.textContent).toBe(raw);
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
expect(bringBack).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps complete Markdown visible when only auxiliary fields could not be recovered', async () => {
|
||||
api.read.mockResolvedValue({ ...first, requests: [request({
|
||||
response: '**它早就不是"刚搭好架子"那种阶段了。** 后面的回答也已经收到。',
|
||||
replyParseError: 'invalid quickReplies', unparsedResponse: '{"reply":"完整回答","quickReplies":',
|
||||
})] });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await ready();
|
||||
expect(screen.getByTestId('teacher-reply')).toHaveTextContent('它早就不是"刚搭好架子"那种阶段了。 后面的回答也已经收到。');
|
||||
expect(screen.getByText('回答正文已保留,部分附加内容未能显示。')).toBeVisible();
|
||||
expect(screen.queryByText('这次回复未能完整显示')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '重新回答' })).not.toBeInTheDocument();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['question', 'suggestions', 'guided-help'] as const)('retries an incomplete %s only on click with its original input while preserving the current draft', async intent => {
|
||||
const originalReferences = [{ messageId: 'original-message', text: '原来的引用', role: 'assistant' as const }];
|
||||
const currentQuote = { messageId: 'current-message', text: '草稿里的新引用', role: 'assistant' as const };
|
||||
const incomplete = request({
|
||||
intent, text: '现在项目是什么阶段?', sourceConversationId: 'original-source', references: originalReferences,
|
||||
response: '它早就不是', replyIncomplete: true,
|
||||
});
|
||||
const before = { ...first, requests: [incomplete] };
|
||||
api.read.mockResolvedValue(before);
|
||||
let finish!: (topic: TeacherTopic) => void;
|
||||
api.send.mockImplementation(() => new Promise<TeacherTopic>(resolve => { finish = resolve; }));
|
||||
render(<TeacherChatPanel projectId="p" sourceId="different-source" quote={currentQuote} />);
|
||||
await ready();
|
||||
const input = screen.getByLabelText('向智能体提问');
|
||||
fireEvent.change(input, { target: { value: '还没说完的新想法' } });
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
const retry = screen.getByRole('button', { name: '重新回答' });
|
||||
fireEvent.click(retry);
|
||||
fireEvent.click(retry);
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
||||
expect(sent).toEqual({ requestId: expect.any(String), text: incomplete.text, intent, references: originalReferences, sourceConversationId: 'original-source' });
|
||||
expect(sent.requestId).not.toBe(incomplete.id);
|
||||
expect(retry).toBeDisabled();
|
||||
fireEvent.change(input, { target: { value: '等待时又补充了想法' } });
|
||||
await act(async () => finish({ ...before, revision: 2, requests: [...before.requests, request({ id: sent.requestId, text: sent.text, response: '这次的完整回答。' })] }));
|
||||
expect(screen.getByText('这次的完整回答。')).toBeVisible();
|
||||
expect(input).toHaveValue('等待时又补充了想法');
|
||||
expect(screen.getByText('草稿里的新引用')).toBeVisible();
|
||||
expect(JSON.parse(localStorage.getItem('makelore-consultation-draft:signed-out:p:teacher')!)).toMatchObject({ text: '等待时又补充了想法', references: [currentQuote] });
|
||||
});
|
||||
|
||||
it('keeps the retry id and draft after an uncertain send failure and uses the original topic source for older replies', async () => {
|
||||
const incomplete = request({ text: '原来的问题', response: '', replyIncomplete: true });
|
||||
const before = { ...first, sourceConversationId: 'older-source', requests: [incomplete] };
|
||||
api.read.mockResolvedValue(before);
|
||||
api.send.mockRejectedValueOnce(new Error('暂时没连上')).mockImplementation(async (_base, _id, sent: TeacherSend) => ({
|
||||
...before, revision: 2, requests: [incomplete, request({ id: sent.requestId, text: sent.text, response: '完整回答' })],
|
||||
}));
|
||||
render(<TeacherChatPanel projectId="p" sourceId="new-source" />);
|
||||
await ready();
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '草稿不能丢' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '重新回答' }));
|
||||
await screen.findByRole('alert');
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('草稿不能丢');
|
||||
fireEvent.click(screen.getByRole('button', { name: '重新回答' }));
|
||||
await screen.findByText('完整回答');
|
||||
expect(api.send).toHaveBeenCalledTimes(2);
|
||||
expect(api.send.mock.calls[0][2]).toEqual(api.send.mock.calls[1][2]);
|
||||
expect(api.send.mock.calls[1][2]).toMatchObject({ text: '原来的问题', sourceConversationId: 'older-source', references: [] });
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('草稿不能丢');
|
||||
});
|
||||
|
||||
it('retries a project-only historical question without attaching another conversation or the project sentinel', async () => {
|
||||
const incomplete = request({ text: '原来的问题', replyIncomplete: true });
|
||||
const before = { ...first, sourceConversationId: 'project', requests: [incomplete] };
|
||||
api.read.mockResolvedValue(before);
|
||||
api.send.mockResolvedValue(before);
|
||||
render(<TeacherChatPanel projectId="p" sourceId="different-source" />);
|
||||
await ready();
|
||||
fireEvent.click(screen.getByRole('button', { name: '重新回答' }));
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalledTimes(1));
|
||||
expect(api.send.mock.calls[0][2]).not.toHaveProperty('sourceConversationId');
|
||||
});
|
||||
|
||||
it('does not retry an incomplete reply from a disabled Agent or turn a check-in into a normal question', async () => {
|
||||
const archived = { ...first, definition: { ...definition, config_id: 'unavailable-agent' }, requests: [
|
||||
request({ response: '', replyIncomplete: true }),
|
||||
request({ id: 'check-in', intent: 'check-in', text: '', response: '', replyIncomplete: true }),
|
||||
] };
|
||||
api.read.mockResolvedValue(archived);
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await ready();
|
||||
expect(screen.getAllByText('这次回复未能完整显示')).toHaveLength(2);
|
||||
expect(screen.getAllByRole('button', { name: '重新回答' })).toHaveLength(1);
|
||||
expect(screen.getByRole('button', { name: '重新回答' })).toBeDisabled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '重新回答' }));
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders formulas and preserves code escapes while rejecting executable links and HTML', async () => {
|
||||
api.read.mockResolvedValue({ ...first, requests: [request({
|
||||
response: '$E=mc^2$\n\n```js\nconst value = "\\n";\n```\n\n[危险](javascript:alert)\n\n<script>alert(1)</script>\n\n\n\n',
|
||||
@@ -323,25 +438,66 @@ describe('teacher side chat', () => {
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('requests contextual suggestions explicitly, hides streaming JSON and preserves the freeform draft', async () => {
|
||||
it.each(['question', 'suggestions'] as const)('keeps duplicate %s quick cards distinct and clickable without replacing the draft', async intent => {
|
||||
const question = '我可以先试哪一步?';
|
||||
const previous = request({ intent, response: '先从你想尝试的地方聊起。', suggestedQuestions: [question, question] });
|
||||
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, response: '我们就聊这一点。' })],
|
||||
}));
|
||||
const errors = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
try {
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
const cards = await screen.findAllByRole('button', { name: question, exact: true });
|
||||
expect(cards).toHaveLength(2);
|
||||
expect(cards[0]).toBeEnabled();
|
||||
expect(cards[1]).toBeEnabled();
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '还没说完的想法' } });
|
||||
fireEvent.click(cards[1]);
|
||||
await screen.findByText('我们就聊这一点。');
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
expect(api.send.mock.calls[0][2]).toEqual({ requestId: expect.any(String), text: question, references: [], sourceConversationId: 'c' });
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('还没说完的想法');
|
||||
expect(screen.getAllByRole('button', { name: question, exact: true })).toHaveLength(2);
|
||||
expect(errors.mock.calls.filter(args => /same key|unique.*key/i.test(args.join(' ')))).toEqual([]);
|
||||
} finally {
|
||||
errors.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('only requests suggestions from the empty composer and preserves a later draft while streaming', 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" />);
|
||||
const help = screen.getByRole('button', { name: '帮我看看', exact: true });
|
||||
await waitFor(() => expect(help).toBeEnabled());
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我自己还想问的另一件事' } });
|
||||
fireEvent.click(help);
|
||||
const input = screen.getByLabelText('向智能体提问');
|
||||
const help = () => screen.getByText('继续看看👀', { selector: 'form .consultation-hint' });
|
||||
await waitFor(() => expect(help()).toBeEnabled());
|
||||
expect(screen.getByTestId('consultation-composer-actions')).toContainElement(help());
|
||||
expect(help()).toHaveAccessibleName('继续看看👀');
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.change(input, { target: { value: '我先自己想想' } });
|
||||
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.change(input, { target: { value: ' ' } });
|
||||
expect(help()).toBeEnabled();
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
expect(help()).toBeEnabled();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.click(help());
|
||||
fireEvent.click(help());
|
||||
|
||||
await screen.findByText('我看看你最近做到了哪里…');
|
||||
expect(screen.queryByText(streamingJson)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '尚未确认的问题' })).not.toBeInTheDocument();
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
expect(api.send).toHaveBeenCalledWith('p/agent-topics', 'first', expect.objectContaining({
|
||||
intent: 'suggestions', text: '帮我看看', sourceConversationId: 'c',
|
||||
intent: 'suggestions', text: '继续看看👀', sourceConversationId: 'c',
|
||||
}));
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我自己还想问的另一件事');
|
||||
expect(screen.getByRole('button', { name: '帮我看看', exact: true })).toBeDisabled();
|
||||
expect(help()).toBeDisabled();
|
||||
fireEvent.change(input, { target: { value: '我自己还想问的另一件事' } });
|
||||
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
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({
|
||||
@@ -351,23 +507,21 @@ describe('teacher side chat', () => {
|
||||
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(screen.queryByRole('button', { name: '我也说不清,你带我看看', exact: true })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我自己还想问的另一件事');
|
||||
expect(input).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();
|
||||
it.each([suggestedQuestions[0], '我也说不清,你带我看看'])('sends the Agent-generated card "%s" as an ordinary question without replacing the draft', async text => {
|
||||
const previous = suggestionsRequest({ suggestedQuestions: [text] });
|
||||
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: '我们先从你刚才做的那一步看起。' })],
|
||||
...first, revision: 2, requests: [previous, request({ id: input.requestId, text: input.text, response: '我们先从你刚才做的那一步看起。' })],
|
||||
}));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" quote={{ kind: 'code', text: '学生正在引用的一段代码' }} />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: text, exact: true })).toBeEnabled());
|
||||
expect(screen.getAllByRole('button', { name: text, exact: true })).toHaveLength(1);
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '还在整理的自由提问' } });
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: text, exact: true }));
|
||||
@@ -376,7 +530,7 @@ describe('teacher side chat', () => {
|
||||
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(sent.intent).toBeUndefined();
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('还在整理的自由提问');
|
||||
expect(screen.getByText('学生正在引用的一段代码')).toBeInTheDocument();
|
||||
view.unmount();
|
||||
@@ -386,30 +540,79 @@ describe('teacher side chat', () => {
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('retries an uncertain suggestions request after reopening without losing the student draft', async () => {
|
||||
it('keeps an uncertain suggestions request identity through reopening and draft edits', async () => {
|
||||
api.send.mockRejectedValue(new Error('网络暂不可用'));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
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 waitFor(() => expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我还没写完的问题' } });
|
||||
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
|
||||
expect(originalRequest.intent).toBe('suggestions');
|
||||
view.unmount();
|
||||
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '帮我看看', exact: true })).toBeEnabled());
|
||||
await waitFor(() => expect(screen.getByLabelText('向智能体提问')).toBeEnabled());
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我还没写完的问题');
|
||||
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我又补了一点自己的想法' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '帮我看看', exact: true }));
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '' } });
|
||||
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
expect(api.send.mock.calls[1][2]).toEqual(originalRequest);
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我又补了一点自己的想法');
|
||||
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: '这次没有看清楚,请再试一次。' });
|
||||
it.each([{ text: '帮我看看', references: undefined }, { text: '给我一点提示', references: [{ kind: 'code' as const, text: '旧请求的引用' }] }])('retries an uncertain legacy hint payload "$text" unchanged with references $references', async ({ text, references }) => {
|
||||
const original: TeacherSend = { requestId: 'legacy-hint-request', text, intent: 'suggestions', sourceConversationId: 'previous-source', ...(references ? { references } : {}) };
|
||||
localStorage.setItem('makelore-consultation-draft:signed-out:p:teacher', JSON.stringify({ text: '', references: [], pending: original }));
|
||||
api.send.mockRejectedValue(new Error('网络暂不可用'));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
expect(JSON.parse(JSON.stringify(api.send.mock.calls[0][2]))).toEqual(original);
|
||||
view.unmount();
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
expect(JSON.parse(JSON.stringify(api.send.mock.calls[1][2]))).toEqual(original);
|
||||
});
|
||||
|
||||
it('does not reuse an uncertain legacy hint request for an ordinary typed question with the same text', async () => {
|
||||
const original: TeacherSend = { requestId: 'legacy-hint-request', text: '帮我看看', intent: 'suggestions', references: [] };
|
||||
localStorage.setItem('makelore-consultation-draft:signed-out:p:teacher', JSON.stringify({ text: original.text, references: [], pending: original }));
|
||||
api.send.mockResolvedValue({ ...first, revision: 2 });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '提问', exact: true })).toBeEnabled());
|
||||
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalledTimes(1));
|
||||
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
||||
expect(sent.requestId).not.toBe(original.requestId);
|
||||
expect(sent.text).toBe(original.text);
|
||||
expect(sent.intent).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each(['completed', 'failed'] as const)('starts a fresh hint when the old hint request is already confirmed %s', async status => {
|
||||
const original: TeacherSend = { requestId: 'legacy-hint-request', text: '帮我看看', intent: 'suggestions' };
|
||||
localStorage.setItem('makelore-consultation-draft:signed-out:p:teacher', JSON.stringify({ text: '', references: [], pending: original }));
|
||||
api.read.mockResolvedValue({ ...first, requests: [suggestionsRequest({ id: original.requestId, text: original.text, status })] });
|
||||
api.send.mockResolvedValue({ ...first, revision: 2 });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalledTimes(1));
|
||||
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
||||
expect(sent.requestId).not.toBe(original.requestId);
|
||||
expect(sent).toMatchObject({ text: '继续看看👀', intent: 'suggestions', references: [], sourceConversationId: 'c' });
|
||||
});
|
||||
|
||||
it.each(['failed', 'cancelled', 'interrupted'] as const)('retries a %s suggestion request without leaking raw JSON or reusing its accepted request id', async status => {
|
||||
const failed = suggestionsRequest({ status, 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 })],
|
||||
@@ -418,7 +621,7 @@ describe('teacher side chat', () => {
|
||||
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 }));
|
||||
fireEvent.click(screen.getByText('继续看看👀', { selector: '.consultation-retry' }));
|
||||
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);
|
||||
@@ -430,11 +633,11 @@ describe('teacher side chat', () => {
|
||||
...first, revision: 3, requests: [accepted, suggestionsRequest({ id: input.requestId })],
|
||||
}));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '帮我看看', exact: true })).toBeEnabled());
|
||||
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
await waitFor(() => expect(streams.has('first')).toBe(true));
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我的自由提问继续留着' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '帮我看看', exact: true }));
|
||||
fireEvent.click(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' }));
|
||||
await screen.findByText('响应中途断开');
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我的自由提问继续留着' } });
|
||||
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] };
|
||||
@@ -446,10 +649,10 @@ describe('teacher side chat', () => {
|
||||
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());
|
||||
await waitFor(() => expect(screen.getByText('继续看看👀', { selector: '.consultation-retry' })).toBeEnabled());
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我的自由提问继续留着');
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByRole('button', { name: '再请智能体看看', exact: true }));
|
||||
fireEvent.click(screen.getByText('继续看看👀', { selector: '.consultation-retry' }));
|
||||
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
|
||||
expect(api.send).toHaveBeenCalledTimes(2);
|
||||
expect(api.send.mock.calls[1][2].requestId).not.toBe(originalRequest.requestId);
|
||||
@@ -472,7 +675,7 @@ describe('teacher side chat', () => {
|
||||
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();
|
||||
expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeEnabled();
|
||||
view.unmount();
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
||||
@@ -560,112 +763,92 @@ describe('teacher side chat', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '运营配置的预览问题' }));
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('运营配置的预览问题');
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole('button', { name: '帮我看看', exact: true })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '智能体新话题' }));
|
||||
await waitFor(() => expect(api.create).toHaveBeenCalledWith('preview/sample', 7, '学生的练习代码', undefined));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('pinned discussion workspace', () => {
|
||||
const ideas = { id: 'tool', revision: 1, status: 'active' as const, content: {
|
||||
kind: 'ideas' as const, title: '一起想我的宠物游戏', items: [{ id: 'dog', text: '养只小狗', state: 'kept' as const }],
|
||||
} };
|
||||
async function openDiscussion(status: 'active' | 'offered' = 'active') {
|
||||
const current = { ...structuredClone(first), discussion: { ...structuredClone(ideas), status } };
|
||||
describe('legacy discussion history compatibility', () => {
|
||||
const contents = [
|
||||
{ kind: 'ideas', title: '旧想法组件', items: [{ id: 'dog', text: '养只小狗', state: 'kept' }] },
|
||||
{ kind: 'structure', title: '旧结构组件', nodes: [{ id: 'dog', label: '照顾小狗' }] },
|
||||
{ kind: 'flow', title: '旧流程组件', nodes: [{ id: 'home', kind: 'event', label: '我回到家' }], edges: [] },
|
||||
{ kind: 'comparison', title: '旧对比组件', columns: [{ id: 'a', label: '主动回应' }], rows: [{ id: 'home', label: '回到家时', cells: [{ columnId: 'a', text: '跑来欢迎我' }] }] },
|
||||
];
|
||||
const histories = contents.flatMap(content => ['active', 'offered', 'paused', 'finished'].map(status => ({ kind: content.kind, status, content })));
|
||||
it.each(histories)('reads $kind / $status history as ordinary replies with working quick cards and free input', async ({ status, content }) => {
|
||||
const current = { ...structuredClone(first), discussion: { id: 'legacy-tool', revision: 3, status, content }, requests: [request({
|
||||
presentation: 'discussion-v1', discussionSnapshot: content, discussionContext: { toolId: 'legacy-tool', revision: 3 },
|
||||
response: '先听听你的想法。', suggestedQuestions: ['怎样知道小狗喜欢我?'],
|
||||
})] } as TeacherTopic;
|
||||
api.read.mockResolvedValue(current);
|
||||
api.send.mockImplementation(async (_base, _id, input: TeacherSend) => ({ ...current, revision: current.revision + 1, requests: [...current.requests, request({ id: input.requestId, text: input.text, response: '我们就从这个问题聊起。' })] }));
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
const card = await screen.findByRole('button', { name: '怎样知道小狗喜欢我?' });
|
||||
expect(screen.getByText('先听听你的想法。')).toBeVisible();
|
||||
expect(screen.queryByTestId('teacher-discussion')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(content.title)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /用这个一起想|先这些|接着改|把想法理一理|聊聊这一点|取消讨论焦点/ })).not.toBeInTheDocument();
|
||||
const input = screen.getByLabelText('向智能体提问');
|
||||
fireEvent.change(input, { target: { value: '自己的草稿' } });
|
||||
fireEvent.click(card);
|
||||
await screen.findByText('我们就从这个问题聊起。');
|
||||
expect(api.send.mock.calls[0][2]).toEqual({ requestId: expect.any(String), text: '怎样知道小狗喜欢我?', references: [], sourceConversationId: 'c' });
|
||||
expect(input).toHaveValue('自己的草稿');
|
||||
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalledTimes(2));
|
||||
expect(api.send.mock.calls[1][2]).toEqual({ requestId: expect.any(String), text: '自己的草稿', references: [], sourceConversationId: 'c' });
|
||||
await waitFor(() => expect(input).toHaveValue(''));
|
||||
expect(screen.queryByTestId('teacher-discussion')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps live text and cancellation working when snapshots still contain legacy component fields', async () => {
|
||||
const current = { ...structuredClone(first), discussion: { id: 'tool', revision: 1, status: 'active', content: contents[0] } } as TeacherTopic;
|
||||
api.read.mockResolvedValue(current);
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await screen.findByText('一起想我的宠物游戏');
|
||||
await waitFor(() => expect(streams.has('first')).toBe(true));
|
||||
return current;
|
||||
}
|
||||
it('quotes a focused item into the existing composer without sending, then sends the locked version', async () => {
|
||||
const current = await openDiscussion();
|
||||
api.send.mockImplementation(async (_base, _id, input) => ({ ...current, revision: 2, requests: [request({ text: input.text, response: '先聊聊小狗。' })] }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '养只小狗', exact: true }));
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
expect(screen.getByText('接着聊:养只小狗')).toBeVisible();
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我希望是柯基' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalledWith('p/agent-topics', 'first', expect.objectContaining({ presentation: 'discussion-v1', discussion: { toolId: 'tool', revision: 1, focusId: 'dog' }, text: '我希望是柯基' })));
|
||||
expect(screen.getAllByTestId('teacher-discussion')).toHaveLength(1);
|
||||
});
|
||||
it('finishes provisionally, keeps the draft, chats without changing the tool, and resumes explicitly', async () => {
|
||||
const current = await openDiscussion();
|
||||
api.updateDiscussion.mockResolvedValueOnce({ ...current, revision: 2, discussion: { ...ideas, status: 'finished', revision: 2 } });
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '还在想的下一句' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '先这些', exact: true }));
|
||||
await screen.findByText('先留在这里');
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('还在想的下一句');
|
||||
expect(screen.getByTestId('teacher-discussion')).toHaveAttribute('data-collapsed', 'true');
|
||||
api.send.mockImplementation(async (_base, _id, input) => ({ ...current, revision: 3, discussion: { ...ideas, status: 'finished', revision: 2 }, requests: [request({ text: input.text, response: '我们慢慢聊。' })] }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
|
||||
await screen.findByText('我们慢慢聊。');
|
||||
expect(api.send.mock.calls[0][2].discussion).toBeUndefined();
|
||||
api.updateDiscussion.mockResolvedValueOnce({ ...current, revision: 4, discussion: { ...ideas, revision: 3 } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '接着改', exact: true }));
|
||||
await screen.findByRole('button', { name: '养只小狗', exact: true });
|
||||
expect(api.updateDiscussion.mock.calls[1][2]).toEqual({ toolId: 'tool', revision: 2, action: 'resume' });
|
||||
});
|
||||
it('offers a tool before activating, and preserves a draft during explicit structure conversion', async () => {
|
||||
const current = await openDiscussion('offered');
|
||||
api.updateDiscussion.mockResolvedValue({ ...current, revision: 2, discussion: { ...ideas, revision: 2 } });
|
||||
expect(screen.getByRole('button', { name: '养只小狗', exact: true })).toBeDisabled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '用这个一起想', exact: true }));
|
||||
await screen.findByRole('button', { name: '把想法理一理', exact: true });
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '不要丢掉的草稿' } });
|
||||
api.send.mockResolvedValue({ ...current, revision: 3, discussion: { ...ideas, revision: 2 }, requests: [] });
|
||||
fireEvent.click(screen.getByRole('button', { name: '把想法理一理', exact: true }));
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalled());
|
||||
expect(api.send.mock.calls[0][2].discussion).toEqual({ toolId: 'tool', revision: 2, transition: 'structure' });
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('不要丢掉的草稿');
|
||||
});
|
||||
it('uses the same pinned component for SSE updates and leaves it untouched while thinking or stopping', async () => {
|
||||
const current = await openDiscussion();
|
||||
await ready();
|
||||
const stream = streams.get('first')!;
|
||||
const updating = { ...current, revision: 2, requests: [request({ status: 'running', response: '', discussionContext: { toolId: 'tool', revision: 1 } })] };
|
||||
act(() => stream.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify(updating) })));
|
||||
expect(screen.getByRole('status')).toHaveTextContent('正在梳理');
|
||||
expect(screen.getByRole('button', { name: '养只小狗', exact: true })).toBeVisible();
|
||||
expect(screen.getByRole('status')).toHaveTextContent('智能体正在想');
|
||||
expect(screen.queryByTestId('teacher-discussion')).not.toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '下一轮草稿' } });
|
||||
api.cancel.mockResolvedValue({ ...updating, revision: 3, requests: [request({ status: 'cancelled' })] });
|
||||
fireEvent.click(screen.getByRole('button', { name: '停止回复' }));
|
||||
await screen.findByText('这次先停在这里。想继续时可以再说说。');
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('下一轮草稿');
|
||||
act(() => stream.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify({ ...current, revision: 4, discussion: { ...ideas, revision: 2, content: { ...ideas.content, title: '一起想小狗和花园' } } }) })));
|
||||
expect(screen.getAllByTestId('teacher-discussion')).toHaveLength(1);
|
||||
expect(screen.getByText('一起想小狗和花园')).toBeVisible();
|
||||
});
|
||||
it('sends a normal suggested question directly without changing the composer draft', async () => {
|
||||
const current = { ...first, requests: [request({ response: '先听听你的想法。', suggestedQuestions: ['怎样知道小狗喜欢我?'] })] };
|
||||
api.read.mockResolvedValue(current);
|
||||
api.send.mockResolvedValue({ ...current, revision: 2 });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await screen.findByRole('button', { name: '怎样知道小狗喜欢我?' });
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '自己的草稿' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '怎样知道小狗喜欢我?' }));
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalled());
|
||||
expect(api.send.mock.calls[0][2].text).toBe('怎样知道小狗喜欢我?');
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('自己的草稿');
|
||||
act(() => stream.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify({ ...current, revision: 4, discussion: { unknownLegacyShape: true }, requests: [request({ response: '旧历史继续显示普通回复。', suggestedQuestions: ['接着问一个问题'] })] }) })));
|
||||
expect(screen.getByText('旧历史继续显示普通回复。')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '接着问一个问题' })).toBeEnabled();
|
||||
expect(screen.queryByTestId('teacher-discussion')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('comparison focuses and directly sends the selected difference while preserving the existing draft', async () => {
|
||||
const current = { ...first, discussion: { id: 'compare', revision: 3, status: 'active' as const, content: {
|
||||
kind: 'comparison' as const, title: '它什么时候回应我?', columns: [{ id: 'a', label: '点击才回应' }, { id: 'b', label: '主动回应' }], rows: [{ id: 'home', label: '回到家时', cells: [{ columnId: 'a', text: '等我点击' }, { columnId: 'b', text: '跑来欢迎我' }] }],
|
||||
} } };
|
||||
api.read.mockResolvedValue(current);
|
||||
api.send.mockResolvedValue({ ...current, revision: 2 });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await screen.findByText('回到家时');
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '自己还没说完的话' } });
|
||||
fireEvent.click(screen.getAllByRole('button', { name: '聊聊这一点' })[1]);
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalled());
|
||||
expect(api.send.mock.calls[0][2]).toMatchObject({ text: '聊聊「回到家时」时,「主动回应」这一点:跑来欢迎我', discussion: { toolId: 'compare', revision: 3, focusId: 'home:b' } });
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('自己还没说完的话');
|
||||
expect(screen.getAllByTestId('teacher-discussion')).toHaveLength(1);
|
||||
it('removes retired fields from a persisted pending send without changing its draft, references or retry id', async () => {
|
||||
const key = 'makelore-consultation-draft:signed-out:p:teacher';
|
||||
const references = [{ kind: 'code', text: 'const pet = "dog";' }];
|
||||
const original = { requestId: 'pending-before-component-removal', text: '继续聊这件事', references, intent: 'question', sourceConversationId: 'previous-source' };
|
||||
localStorage.setItem(key, JSON.stringify({ text: original.text, references, pending: { ...original, presentation: 'discussion-v1', discussion: { toolId: 'tool', revision: 2, focusId: 'dog', transition: 'structure' } } }));
|
||||
api.send.mockRejectedValueOnce(new Error('网络暂不可用'));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '提问', exact: true })).toBeEnabled());
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue(original.text);
|
||||
expect(screen.getByText(references[0].text)).toBeVisible();
|
||||
expect(JSON.parse(localStorage.getItem(key)!).pending).toEqual(original);
|
||||
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
expect(api.send.mock.calls[0][2]).toEqual(original);
|
||||
view.unmount();
|
||||
api.send.mockResolvedValue({ ...first, revision: 2, requests: [request({ id: original.requestId, text: original.text, response: '可以,我们继续。' })] });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '提问', exact: true })).toBeEnabled());
|
||||
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
|
||||
await screen.findByText('可以,我们继续。');
|
||||
expect(api.send.mock.calls[1][2]).toEqual(original);
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
||||
expect(screen.queryByText(references[0].text)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -709,7 +892,7 @@ describe('merged cloud teacher classroom', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
|
||||
await screen.findByText('先看看玩家会做什么。');
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
expect(api.sendConversation).toHaveBeenCalledWith('algorithm', expect.objectContaining({ projectId: 'p', sourceConversationId: 'c', presentation: 'discussion-v1' }));
|
||||
expect(api.sendConversation).toHaveBeenCalledWith('algorithm', expect.objectContaining({ projectId: 'p', sourceConversationId: 'c' }));
|
||||
expect(screen.getByRole('complementary', { name: '算法智能体咨询' })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -718,9 +901,10 @@ describe('merged cloud teacher classroom', () => {
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText('智能体暂未开放,历史仍可查看。')).toBeVisible());
|
||||
const input = screen.getByLabelText('向智能体提问');
|
||||
expect(screen.getByText('继续看看👀', { selector: 'form .consultation-hint' })).toBeDisabled();
|
||||
fireEvent.change(input, { target: { value: '继续讨论' } });
|
||||
expect(screen.getByRole('button', { name: '提问', exact: true })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: '帮我看看', exact: true })).toBeDisabled();
|
||||
expect(screen.queryByText('继续看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user