// @vitest-environment jsdom import { act, cleanup, renderHook } from '@testing-library/react'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import type { DesignView } from '../src/app/types'; import { useDesignReplyAnimation } from '../src/view/project-development/planning/useDesignReplyAnimation'; beforeEach(() => vi.useFakeTimers()); afterEach(() => { cleanup(); vi.useRealTimers(); }); function view(messages: DesignView['messages'], running = false): DesignView { return { session: { sessionId: 's', projectId: 'p', currentPhase: 'concept', approvedPhases: [], pendingApproval: null, pendingClarification: null, turnIndex: 1, lastError: null, }, messages, running, canRetry: false, }; } it('keeps each reply progress across tool snapshots and repeated completion', () => { const { result } = renderHook(useDesignReplyAnimation); const history = { id: 'history', role: 'assistant', text: '历史' }; const first = { id: 't:response:0', role: 'assistant', text: '第一条回复' }; const second = { id: 't:response:1', role: 'assistant', text: '第二条回复' }; act(() => { result.current.reset([history.id]); result.current.receiveText(first.id, first.text); vi.advanceTimersByTime(100); }); expect(result.current.replies[0].visible).toBe('第一'); act(() => result.current.receiveView( view( [history, first, { id: 'tool', role: 'tool', text: '工具已完成' }], true, ), ), ); expect(result.current.replies).toHaveLength(1); expect(result.current.replies[0].visible).toBe('第一'); act(() => result.current.receiveText(second.id, second.text)); const terminal = view([history, first, second]); act(() => { result.current.receiveView(terminal); result.current.receiveView(terminal); }); expect(result.current.replies.map((reply) => reply.visible)).toEqual([ '第一', '', ]); act(() => vi.advanceTimersByTime(500)); act(() => result.current.receiveView(terminal)); expect(result.current.replies.map((reply) => reply.visible)).toEqual([ first.text, second.text, ]); }); it('resets only an unpersisted retry attempt and discards failed partial output', () => { const { result } = renderHook(useDesignReplyAnimation); act(() => { result.current.receiveText('t:response:0', '尝试失败'); vi.advanceTimersByTime(100); }); expect(result.current.replies[0].visible).toBe('尝试'); act(() => result.current.receiveText('t:response:0', '')); expect(result.current.replies[0].visible).toBe(''); const saved = { id: 't:response:0', role: 'assistant', text: '成功' }; act(() => { result.current.receiveText(saved.id, saved.text); result.current.receiveView(view([saved], true)); vi.advanceTimersByTime(100); result.current.receiveText(saved.id, ''); result.current.receiveText('t:response:1', '未被接受的文本'); result.current.receiveView(view([saved])); }); expect(result.current.replies).toEqual([ { messageId: saved.id, target: '成功', visible: '成功', persisted: true }, ]); act(() => { result.current.receiveText('t:response:2', '连接中断'); result.current.discardUnpersisted(); }); expect(result.current.replies).toHaveLength(1); act(() => result.current.reset([saved.id])); expect(result.current.replies).toEqual([]); });