diff --git a/docs/technical/【前端测试】React组件测试准则-2026-06-26.md b/docs/technical/【前端测试】React组件测试准则-2026-06-26.md
index 6b32b7ce5..eca1179c6 100644
--- a/docs/technical/【前端测试】React组件测试准则-2026-06-26.md
+++ b/docs/technical/【前端测试】React组件测试准则-2026-06-26.md
@@ -38,6 +38,8 @@
- 当测试是为防止历史回归,应在测试名或邻近注释中说明防的是什么行为,而不是记录实现步骤。
- 同一用例既要验证定时器调度参数,又要断言确定的中间帧或中间状态时,必须 mock 定时器回调或使用可控假时钟;不得让真实墙上时间在异步交互期间推进被断言的状态,否则本地通过的用例会在较慢 CI 中偶发失败。
- 测试 React effect 中注册的事件监听时,触发事件前先用可观测的 listener 调用确认注册已完成;异步请求已开始应用有界 `waitFor` 断言确认,不要用无界手工 Promise 等待一次性信号。解除挂起请求时将 Promise 收尾纳入异步 `act`,确保后续 React 更新在断言前已冲刷。
+- 公共 `AutoGrowTextArea` 使用 Lexical `contenteditable`,业务测试通过 `setPlainTextEditorValue` 写入文本、通过 `getPlainTextEditorHost` 断言外层样式;占位文案是独立渲染节点,不读取原生 `placeholder` 属性,也不对编辑根触发 textarea 专属的 `change` 事件。
+- 异步请求失败时,错误状态与调用方的草稿 / 附件恢复可能分属连续两次 React 更新;用例必须对最终恢复结果使用有界 `waitFor`,不能把错误文案刚出现的中间帧当成恢复已经完成。
## 试点调整
diff --git a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx
index 1eeabed63..51c8a35cc 100644
--- a/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx
+++ b/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.test.tsx
@@ -1377,8 +1377,10 @@ describe('EditorAgentConversationPanelView', () => {
fireEvent.click(screen.getByRole('button', { name: '发送' }));
expect(await screen.findByText('Network error')).toBeTruthy();
- expect(screen.getByLabelText('发送给画布 Agent').textContent).toBe(
- '失败后恢复这条草稿',
+ await waitFor(() =>
+ expect(screen.getByLabelText('发送给画布 Agent').textContent).toBe(
+ '失败后恢复这条草稿',
+ ),
);
expect(screen.getByText('角色图层')).toBeTruthy();
expect(
diff --git a/src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx b/src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx
index d99b8bc9d..8946494bc 100644
--- a/src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx
+++ b/src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx
@@ -4,6 +4,10 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { createRef, useState } from 'react';
import { describe, expect, it, vi } from 'vitest';
+import {
+ getPlainTextEditorHost,
+ setPlainTextEditorValue,
+} from '../common/AutoGrowTextArea.test-utils';
import { ImageCanvasCharacterGenerationComposerView } from './ImageCanvasCharacterGenerationComposerView';
import type {
GenerateDialogState,
@@ -94,21 +98,26 @@ function CharacterGenerationHarness({
describe('ImageCanvasCharacterGenerationComposerView', () => {
it('keeps the prompt as a bordered single text input with a question placeholder', () => {
- render();
+ render(
+ ,
+ );
const panel = screen.getByRole('dialog', { name: '生成角色形象' });
const prompt = screen.getByRole('textbox', { name: '角色设定' });
+ const promptHost = getPlainTextEditorHost(prompt);
expect(
Array.from(panel.querySelectorAll('.image-canvas-editor__field-title'))
.map((node) => node.textContent)
.join(''),
).not.toContain('角色设定');
- expect(prompt.getAttribute('placeholder')).toBe('你希望角色如何设计?');
- expect(prompt.className).toContain('auto-grow-text-area');
- expect(prompt.className).not.toContain('platform-text-field');
- expect(prompt.className).toContain('image-canvas-editor__generation-prompt');
- expect(prompt.className).not.toContain(
+ expect(screen.getByText('你希望角色如何设计?')).toBeTruthy();
+ expect(promptHost.className).toContain('auto-grow-text-area');
+ expect(promptHost.className).not.toContain('platform-text-field');
+ expect(promptHost.className).toContain(
+ 'image-canvas-editor__generation-prompt',
+ );
+ expect(promptHost.className).not.toContain(
'image-canvas-editor__generation-prompt--borderless',
);
});
@@ -125,9 +134,10 @@ describe('ImageCanvasCharacterGenerationComposerView', () => {
/>,
);
- fireEvent.change(screen.getByLabelText('角色设定'), {
- target: { value: '新的角色设定' },
- });
+ setPlainTextEditorValue(
+ screen.getByLabelText('角色设定'),
+ '新的角色设定',
+ );
fireEvent.click(screen.getByRole('button', { name: '生成' }));
expect(screen.getByLabelText('当前角色设定').textContent).toBe(
diff --git a/src/components/image-editor/ImageCanvasEditorView.test.tsx b/src/components/image-editor/ImageCanvasEditorView.test.tsx
index 13d07c3ef..158f2bea4 100644
--- a/src/components/image-editor/ImageCanvasEditorView.test.tsx
+++ b/src/components/image-editor/ImageCanvasEditorView.test.tsx
@@ -12,8 +12,8 @@ import userEvent from '@testing-library/user-event';
import JSZip from 'jszip';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils';
import { usePlatformWalletStore } from '../../stores/usePlatformWalletStore';
+import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils';
import type { EditorAgentConversationClient } from './EditorAgentConversation/useEditorAgentConversation';
import {
ApiClientError,