import { beforeEach, describe, expect, it } from 'vitest' import { act, fireEvent, render, screen } from '@testing-library/react' import { ThinkingBlock } from './ThinkingBlock' import { ToolCallBlock } from './ToolCallBlock' import { PermissionDialog } from './PermissionDialog' import { useChatStore } from '../../stores/chatStore' import { useSettingsStore } from '../../stores/settingsStore' import { useTabStore } from '../../stores/tabStore' describe('chat blocks', () => { beforeEach(() => { useSettingsStore.setState({ locale: 'en' }) useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', type: 'session' as const, status: 'idle' }] }) useChatStore.setState({ sessions: {} }) }) it('keeps thinking collapsed by default', () => { const { container } = render() expect(screen.getByText(/Thinking/)).toBeTruthy() expect(container.textContent).not.toContain('this is a long internal reasoning trace') expect(container.querySelector('.thinking-cursor')).toBeNull() }) it('does not animate inactive historical thinking blocks', () => { const { container } = render() fireEvent.click(screen.getByRole('button', { name: /Thinking/ })) expect(container.textContent).toContain('old reasoning') expect(container.querySelector('.thinking-cursor')).toBeNull() }) it('renders thinking content as markdown only after expanding', () => { const { container } = render() expect(container.textContent).not.toContain('important') expect(container.querySelector('strong')).toBeNull() expect(container.querySelector('li')).toBeNull() fireEvent.click(screen.getByRole('button', { name: /Thinking/ })) expect(container.querySelector('strong')?.textContent).toBe('important') expect(container.querySelector('li')?.textContent).toBe('item one') }) it('hides full thinking content until expanded', () => { const content = Array.from({ length: 12 }, (_, index) => `line-${index + 1}`).join('\n') const { container } = render() expect(container.textContent).not.toContain('line-1') expect(container.textContent).not.toContain('line-11') fireEvent.click(screen.getByRole('button', { name: /Thinking/ })) expect(container.textContent).toContain('line-1') expect(container.textContent).toContain('line-11') expect(container.textContent).toContain('line-12') }) it('shows tool previews only after expanding the tool block', () => { const { container } = render( , ) expect(container.textContent).toContain('Read') expect(container.textContent).not.toContain('const answer = 42') fireEvent.click(screen.getByRole('button')) expect(container.textContent).toContain('Tool Input') expect(container.textContent).not.toContain('const answer = 42') }) it('does not surface bash stdout in the transcript preview', () => { const { container } = render( , ) expect(container.textContent).toContain('Bash') expect(container.textContent).not.toContain('file-a') fireEvent.click(screen.getByRole('button')) expect(container.textContent).toContain('ls -la') expect(container.textContent).not.toContain('file-a') }) it('shows pending Write tool calls while input is still streaming', () => { const { container } = render( , ) expect(container.textContent).toContain('Write') expect(container.textContent).toContain('ai-code-novel.md') expect(container.textContent).toContain('Generating content') }) it('expands pending Write tool calls into a live writer preview instead of raw JSON', () => { const { container } = render( , ) fireEvent.click(screen.getByRole('button')) expect(container.textContent).toContain('Writer') expect(container.textContent).toContain('# 第一章') expect(container.textContent).toContain('正文正在生成') expect(container.textContent).not.toContain('"content"') }) it('windows long pending Write previews to the latest content', () => { const lines = Array.from({ length: 180 }, (_, index) => `line-${index + 1}`) const escapedContent = lines.join('\\n') const { container } = render( , ) fireEvent.click(screen.getByRole('button')) expect(container.textContent).toContain('latest') expect(container.textContent).toContain('line-180') expect(container.textContent).not.toContain('line-30') }) it('shows a collapsed error summary for failed bash commands', () => { const { container } = render( , ) expect(container.textContent).toContain('Bash') expect(container.textContent).toContain('fatal: unrecognized argument: --no-stat') }) it('expands tool errors so full Computer Use gate messages are readable', () => { const { container } = render( , ) expect(container.textContent).toContain('mcp__computer-use__left_click') expect(container.textContent).not.toContain('Take a new screenshot') fireEvent.click(screen.getByRole('button')) expect(container.textContent).toContain('Take a new screenshot') expect(container.textContent).toContain('allowed applications') }) it('shows a diff preview for edit permission requests', async () => { useChatStore.setState({ sessions: { 'active-tab': { messages: [], chatState: 'idle', connectionState: 'connected', streamingText: '', streamingToolInput: '', activeToolUseId: null, activeToolName: null, activeThinkingId: null, pendingPermission: { requestId: 'perm-1', toolName: 'Edit', input: { file_path: '/tmp/example.ts', old_string: 'const count = 1', new_string: 'const count = 2', }, }, pendingComputerUsePermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, elapsedTimer: null, }, }, }) let container!: HTMLElement await act(async () => { container = render( , ).container await Promise.resolve() }) expect(container.textContent).toContain('/tmp/example.ts') expect(container.textContent).toContain('Allow') // react-diff-viewer-continued uses styled-components tables that don't // fully render in jsdom, so we verify the DiffViewer wrapper is mounted expect(container.querySelector('[class*="rounded-[var(--radius-lg)]"]')).toBeTruthy() }) })