// @vitest-environment jsdom import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent, render } from '@testing-library/react' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx' import { zh } from '../src/client/locales.ts' let nextAnimationFrameId = 1 let animationFrames = new Map() function flushAnimationFrames(count: number): void { for (let index = 0; index < count; index += 1) { const callbacks = [...animationFrames.values()] animationFrames.clear() for (const callback of callbacks) callback(index) } } beforeEach(() => { nextAnimationFrameId = 1 animationFrames = new Map() vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { const id = nextAnimationFrameId nextAnimationFrameId += 1 animationFrames.set(id, callback) return id }) vi.stubGlobal('cancelAnimationFrame', (id: number) => { animationFrames.delete(id) }) }) afterEach(() => { cleanup() vi.unstubAllGlobals() }) const t = makeTranslate(zh, commonZh) describe('ReasoningRow', () => { it('follows the latest streaming line, scrolls to its end, then restores the settled first line', () => { const view = render( , ) expect(view.getByText('运行中')).toBeTruthy() const summary = view.getByText('Newest reasoning tokens') Object.defineProperties(summary, { scrollWidth: { configurable: true, value: 300 }, clientWidth: { configurable: true, value: 100 }, }) view.rerender( , ) expect(summary.scrollLeft).toBe(0) flushAnimationFrames(2) expect(summary.scrollLeft).toBe(0) flushAnimationFrames(1) expect(summary.scrollLeft).toBe(200) expect(summary.getAttribute('data-follow-end')).toBe('true') view.rerender( , ) flushAnimationFrames(3) expect(view.getByText('Inspect the session')).toBeTruthy() expect(view.queryByText('运行中')).toBeNull() expect(summary.scrollLeft).toBe(0) expect(summary.hasAttribute('data-follow-end')).toBe(false) }) it('expands from either Think or the reasoning summary', () => { const view = render( , ) const row = view.getByRole('button') fireEvent.click(view.getByText('Inspect the session')) expect(row.getAttribute('aria-expanded')).toBe('true') expect(view.getByText(/Check persistence/)).toBeTruthy() fireEvent.click(view.getByText('Think')) expect(row.getAttribute('aria-expanded')).toBe('false') }) it('expanded Think drops the inline summary and renders plain prose, no IN card', () => { const view = render( , ) fireEvent.click(view.getByText('Think')) expect(view.getAllByText(/Inspect the session/)).toHaveLength(1) expect(view.queryByText('IN')).toBeNull() expect(view.container.querySelector('[class*="ioCard"]')).toBeNull() expect(view.container.querySelector('[class*="thinkBody"]')).not.toBeNull() }) })