ManimCat / frontend /src /studio /components /StudioCommandPanel.test.tsx
Bin29's picture
Sync from main: c1ef036 chore: document docker persistence volumes
94e1b2f
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { StudioCommandPanel } from './StudioCommandPanel'
import type { StudioMessage, StudioSession } from '../protocol/studio-agent-types'
vi.mock('../../i18n', () => ({
useI18n: () => ({
t: (_key: string) => '输入指令...',
}),
}))
function createSession(): StudioSession {
const now = '2026-03-22T00:00:00.000Z'
return {
id: 'session-1',
projectId: 'project-1',
agentType: 'builder',
title: 'Studio',
directory: 'D:/projects/ManimCat',
permissionLevel: 'L2',
permissionRules: [],
createdAt: now,
updatedAt: now,
}
}
function createAssistantMessage(): Extract<StudioMessage, { role: 'assistant' }> {
const now = '2026-03-22T00:00:00.000Z'
return {
id: 'message-1',
sessionId: 'session-1',
role: 'assistant',
agent: 'builder',
parts: [],
createdAt: now,
updatedAt: now,
}
}
describe('StudioCommandPanel', () => {
it('restores the input when submit fails', async () => {
const onRun = vi.fn(async () => {
throw new Error('submit failed')
})
render(
<StudioCommandPanel
session={createSession()}
messages={[]}
latestAssistantText=""
isBusy={false}
disabled={false}
onRun={onRun}
onExit={vi.fn()}
/>,
)
const input = screen.getByPlaceholderText('输入指令...') as HTMLInputElement
fireEvent.change(input, { target: { value: 'render current file' } })
fireEvent.keyDown(input, { key: 'Enter' })
await waitFor(() => expect(onRun).toHaveBeenCalledWith('render current file'))
await waitFor(() => expect(input.value).toBe('render current file'))
})
it('does not flash the full assistant text before typing starts', async () => {
vi.useFakeTimers()
const originalRequestAnimationFrame = window.requestAnimationFrame
const originalCancelAnimationFrame = window.cancelAnimationFrame
window.requestAnimationFrame = (callback: FrameRequestCallback) => window.setTimeout(() => callback(performance.now()), 0)
window.cancelAnimationFrame = (id: number) => window.clearTimeout(id)
try {
render(
<StudioCommandPanel
session={createSession()}
messages={[createAssistantMessage()]}
latestAssistantText="你好,世界"
isBusy
disabled={false}
onRun={vi.fn()}
onExit={vi.fn()}
/>,
)
expect(screen.queryByText('你好,世界')).not.toBeInTheDocument()
await act(async () => {
vi.runOnlyPendingTimers()
})
expect(screen.getByText(/你/)).toBeInTheDocument()
expect(screen.queryByText('你好,世界')).not.toBeInTheDocument()
} finally {
window.requestAnimationFrame = originalRequestAnimationFrame
window.cancelAnimationFrame = originalCancelAnimationFrame
vi.useRealTimers()
}
})
it('does not auto-scroll again for each typing animation step', async () => {
vi.useFakeTimers()
const originalRequestAnimationFrame = window.requestAnimationFrame
const originalCancelAnimationFrame = window.cancelAnimationFrame
const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView
const scrollIntoView = vi.fn()
window.requestAnimationFrame = (callback: FrameRequestCallback) => window.setTimeout(() => callback(performance.now()), 0)
window.cancelAnimationFrame = (id: number) => window.clearTimeout(id)
window.HTMLElement.prototype.scrollIntoView = scrollIntoView
try {
render(
<StudioCommandPanel
session={createSession()}
messages={[createAssistantMessage()]}
latestAssistantText="正在生成内容"
isBusy
disabled={false}
onRun={vi.fn()}
onExit={vi.fn()}
/>,
)
await act(async () => {
vi.runOnlyPendingTimers()
})
const initialCalls = scrollIntoView.mock.calls.length
await act(async () => {
vi.advanceTimersByTime(2000)
})
expect(scrollIntoView.mock.calls.length).toBe(initialCalls)
} finally {
window.requestAnimationFrame = originalRequestAnimationFrame
window.cancelAnimationFrame = originalCancelAnimationFrame
window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView
vi.useRealTimers()
}
})
it('hides stale empty assistant placeholders once a real assistant reply exists', () => {
const now = '2026-03-22T00:00:00.000Z'
render(
<StudioCommandPanel
session={createSession()}
messages={[
{
id: 'message-empty',
sessionId: 'session-1',
role: 'assistant',
agent: 'builder',
parts: [],
createdAt: now,
updatedAt: now,
},
{
id: 'message-real',
sessionId: 'session-1',
role: 'assistant',
agent: 'builder',
parts: [
{
id: 'part-1',
messageId: 'message-real',
sessionId: 'session-1',
type: 'text',
text: '正式回复',
},
],
createdAt: now,
updatedAt: now,
},
]}
latestAssistantText=""
isBusy={false}
disabled={false}
onRun={vi.fn()}
onExit={vi.fn()}
/>,
)
expect(screen.getByText('正式回复')).toBeInTheDocument()
expect(screen.queryByText('暂无响应输出')).not.toBeInTheDocument()
})
})