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,