修复澄清自由输入把整页打进 error boundary
Project CI / Frontend tests (pull_request) Successful in 3m43s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / Native shell tests (pull_request) Successful in 14m6s

澄清卡片的自由回答输入框在 setAnswers 的 updater 里读
event.currentTarget.value。React 在事件派发结束后会把 currentTarget
置空,而 updater 要等渲染阶段才跑,读到的就是 null,抛出
"Cannot read properties of null (reading 'value')"。异常出在渲染阶段,
被 AuthenticatedClient 的 error boundary 接住,整页被换成
"客户端页面加载失败"。

手动输入、以及先选中选项再删除填充文本,走的都是这一个 onChange。

改成在事件里先取出 value 再交给 updater。新增的回归测试在 StrictMode
下渲染卡片(与 main.tsx 一致),覆盖这两条路径。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 15:36:02 +00:00
parent 1335b64d6b
commit c489c92084
2 changed files with 81 additions and 4 deletions
@@ -251,12 +251,15 @@ export function AgentRuntimeUserInputCard({
placeholder="填写其他答案"
rows={2}
value={answer}
onChange={(event) =>
onChange={(event) => {
// React 会在事件派发结束后把 currentTarget 置空,而 setState 的
// updater 要等到渲染阶段才跑,所以必须在这里先取出值。
const { value } = event.currentTarget;
setAnswers((current) => ({
...current,
[question.id]: event.currentTarget.value,
}))
}
[question.id]: value,
}));
}}
/>
</fieldset>
);
@@ -0,0 +1,74 @@
// @vitest-environment jsdom
import { fireEvent, render, screen } from '@testing-library/react';
import { StrictMode } from 'react';
import { describe, expect, it } from 'vitest';
import type { AgentRuntimeUserInputRequest } from '../src/app/types';
import { AgentRuntimeUserInputCard } from '../src/features/agent-runtime/panels';
function clarificationRequest(): AgentRuntimeUserInputRequest {
return {
schemaVersion: 'agc.user_input_request.v1',
requestId: 'req-1',
agentId: 'project-supervisor',
taskId: 'task-1',
sessionId: 'session-1',
runId: 'run-1',
actionId: 'action-1',
status: 'pending',
questions: [
{
id: 'q1',
header: '第1轮·关键决定',
question: '这局游戏的重玩动力是什么?',
options: [
{ label: '分数驱动', description: '刷新纪录后重开' },
{ label: '成长驱动', description: '解锁新能力后重开' },
],
},
],
allowFreeform: true,
responseId: null,
requestedAt: 1,
updatedAt: 1,
};
}
describe('AgentRuntimeUserInputCard 澄清输入', () => {
it('手动输入自由回答不会让页面崩溃', () => {
render(
<StrictMode>
<AgentRuntimeUserInputCard
request={clarificationRequest()}
controlBusy={false}
/>
</StrictMode>,
);
const textarea = screen.getByLabelText('第1轮·关键决定 其他回答');
fireEvent.change(textarea, { target: { value: '玩家自己写的答案' } });
expect((textarea as HTMLTextAreaElement).value).toBe('玩家自己写的答案');
});
it('先选中选项再清空填充文本不会让页面崩溃', () => {
render(
<StrictMode>
<AgentRuntimeUserInputCard
request={clarificationRequest()}
controlBusy={false}
/>
</StrictMode>,
);
fireEvent.click(screen.getByText('分数驱动'));
const textarea = screen.getByLabelText(
'第1轮·关键决定 其他回答',
) as HTMLTextAreaElement;
expect(textarea.value).toBe('分数驱动');
fireEvent.change(textarea, { target: { value: '' } });
expect(textarea.value).toBe('');
});
});