import { describe, it, expect, vi } from 'vitest' import { fireEvent, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' import { skillsApi } from '../api/skills' import { mcpApi } from '../api/mcp' import { useUIStore } from '../stores/uiStore' vi.mock('../api/skills', () => ({ skillsApi: { list: vi.fn(async () => ({ skills: [] })), }, })) vi.mock('../api/mcp', () => ({ mcpApi: { list: vi.fn(async () => ({ servers: [] })), }, })) // Import all pages import { EmptySession } from '../pages/EmptySession' import { ActiveSession } from '../pages/ActiveSession' import { AgentTeams } from '../pages/AgentTeams' import { ScheduledTasks } from '../pages/ScheduledTasks' import { ToolInspection } from '../pages/ToolInspection' // Layout components (chrome is now here, not in pages) import { Sidebar } from '../components/layout/Sidebar' import { UserMessage } from '../components/chat/UserMessage' import { useChatStore } from '../stores/chatStore' import { useSessionStore } from '../stores/sessionStore' import { useTabStore } from '../stores/tabStore' /** * Core rendering tests: content-only pages must render without crashing * and contain key structural elements from the prototype. */ describe('Content-only pages render without errors', () => { it('EmptySession slash picker includes dynamic skills before the first session starts', async () => { vi.mocked(skillsApi.list).mockResolvedValueOnce({ skills: [ { name: 'lark-mail', description: 'Draft, send, and search emails', source: 'user', userInvocable: true, contentLength: 120, hasDirectory: true, }, { name: 'internal-only', description: 'Should stay hidden', source: 'user', userInvocable: false, contentLength: 60, hasDirectory: true, }, ], }) render() fireEvent.change(screen.getByRole('textbox'), { target: { value: '/', selectionStart: 1 }, }) expect(await screen.findByText('/lark-mail')).toBeInTheDocument() expect(screen.queryByText('/internal-only')).not.toBeInTheDocument() }) it('EmptySession renders mascot and composer', () => { const { container } = render() expect(container.querySelector('textarea')).toBeInTheDocument() expect(container.innerHTML).toContain('New session') expect(container.innerHTML).toContain('Ask anything') }) it('EmptySession plus menu exposes uploads and slash commands before chat starts', () => { render() fireEvent.click(screen.getByRole('button', { name: 'Open composer tools' })) expect(screen.getByText('Add files or photos')).toBeInTheDocument() expect(screen.getByText('Slash commands')).toBeInTheDocument() }) it('ActiveSession renders with chat components', () => { const SESSION_ID = 'test-active-session' useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', type: 'session' as const, status: 'idle' }], activeTabId: SESSION_ID }) useChatStore.setState({ sessions: { [SESSION_ID]: { messages: [], chatState: 'idle', connectionState: 'connected', streamingText: '', streamingToolInput: '', activeToolUseId: null, activeToolName: null, activeThinkingId: null, pendingPermission: null, pendingComputerUsePermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, elapsedTimer: null, }, }, }) const { container } = render() // With empty messages, the hero is shown expect(container.innerHTML).toContain('New session') // ChatInput has a textarea const textarea = container.querySelector('textarea') expect(textarea).toBeInTheDocument() expect(textarea).toHaveAttribute('placeholder', 'Ask anything...') expect(textarea).toHaveAttribute('rows', '2') expect(container.innerHTML).not.toContain('Preview') // Cleanup useTabStore.setState({ tabs: [], activeTabId: null }) useChatStore.setState({ sessions: {} }) }) it('ActiveSession keeps the compact composer once messages exist', () => { const SESSION_ID = 'test-active-session-with-messages' useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', type: 'session' as const, status: 'idle' }], activeTabId: SESSION_ID }) useChatStore.setState({ sessions: { [SESSION_ID]: { messages: [{ id: 'msg-1', type: 'user_text', content: 'hello', timestamp: Date.now(), }], chatState: 'idle', connectionState: 'connected', streamingText: '', streamingToolInput: '', activeToolUseId: null, activeToolName: null, activeThinkingId: null, pendingPermission: null, pendingComputerUsePermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, elapsedTimer: null, }, }, }) useSessionStore.setState({ sessions: [{ id: SESSION_ID, title: 'Test', createdAt: '2026-04-10T00:00:00.000Z', modifiedAt: '2026-04-10T00:00:00.000Z', messageCount: 1, projectPath: '', workDir: null, workDirExists: true, }], activeSessionId: SESSION_ID, isLoading: false, error: null, }) render() const textarea = screen.getByPlaceholderText('Ask Claude to edit, debug or explain...') expect(textarea).toHaveAttribute('rows', '1') useTabStore.setState({ tabs: [], activeTabId: null }) useSessionStore.setState({ sessions: [], activeSessionId: null, isLoading: false, error: null }) useChatStore.setState({ sessions: {} }) }) it('ActiveSession shows a single primary action button while a turn is active', () => { useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', type: 'session' as const, status: 'idle' }] }) useChatStore.setState({ sessions: { 'active-tab': { messages: [], chatState: 'thinking', connectionState: 'connected', streamingText: '', streamingToolInput: '', activeToolUseId: null, activeToolName: null, activeThinkingId: null, pendingPermission: null, pendingComputerUsePermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, elapsedTimer: null, }, }, }) render() expect(screen.getByRole('button', { name: /stop/i })).toBeInTheDocument() expect(screen.queryByRole('button', { name: /^run$/i })).not.toBeInTheDocument() useChatStore.setState({ sessions: {} }) }) it('ActiveSession opens a local /mcp panel and clicking an item routes to settings', async () => { const SESSION_ID = 'mcp-panel-session' const sendMessage = vi.fn() vi.mocked(mcpApi.list).mockResolvedValueOnce({ servers: [ { name: 'deepwiki', scope: 'user', transport: 'http', enabled: true, status: 'connected', statusLabel: 'Connected', configLocation: '/tmp/config', summary: 'https://mcp.deepwiki.com/mcp', canEdit: true, canRemove: true, canReconnect: true, canToggle: true, config: { type: 'http', url: 'https://mcp.deepwiki.com/mcp', headers: {} }, }, ], }) useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', type: 'session' as const, status: 'idle' }], activeTabId: SESSION_ID }) useSessionStore.setState({ sessions: [{ id: SESSION_ID, title: 'Test', createdAt: '2026-04-10T00:00:00.000Z', modifiedAt: '2026-04-10T00:00:00.000Z', messageCount: 0, projectPath: '/workspace/project', workDir: '/workspace/project', workDirExists: true, }], activeSessionId: SESSION_ID, isLoading: false, error: null, }) useChatStore.setState({ sessions: { [SESSION_ID]: { messages: [], chatState: 'idle', connectionState: 'connected', streamingText: '', streamingToolInput: '', activeToolUseId: null, activeToolName: null, activeThinkingId: null, pendingPermission: null, pendingComputerUsePermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, elapsedSeconds: 0, statusVerb: '', slashCommands: [{ name: 'mcp', description: 'List available MCP tools' }], agentTaskNotifications: {}, elapsedTimer: null, }, }, sendMessage, }) render() const textarea = screen.getByPlaceholderText('Ask anything...') fireEvent.change(textarea, { target: { value: '/mcp', selectionStart: 4 } }) fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter' }) expect(sendMessage).not.toHaveBeenCalled() expect(await screen.findByText('Available MCP tools')).toBeInTheDocument() fireEvent.click(screen.getByText('deepwiki')) expect(useTabStore.getState().activeTabId).toBe('__settings__') expect(useUIStore.getState().pendingSettingsTab).toBe('mcp') useTabStore.setState({ tabs: [], activeTabId: null }) useSessionStore.setState({ sessions: [], activeSessionId: null, isLoading: false, error: null }) useChatStore.setState({ sessions: {} }) }) it('AgentTeams renders team strip and members', () => { const { container } = render() expect(container.innerHTML).toContain('Architect') expect(container.innerHTML).toContain('session-dev') expect(container.innerHTML).toContain('groups') }) it('ScheduledTasks renders (store-connected)', async () => { const { container } = render() await screen.findByText('Scheduled tasks') expect(container.innerHTML).toContain('Scheduled tasks') }) it('ToolInspection renders diff viewer', () => { const { container } = render() expect(container.innerHTML).toContain('edit_file') expect(container.innerHTML).toContain('Split') expect(container.innerHTML).toContain('Unified') }) }) describe('Chat attachments', () => { it('UserMessage opens image gallery when an attachment is clicked', () => { render( , ) fireEvent.click(screen.getByRole('button')) expect(screen.getByText('diagram.png')).toBeInTheDocument() }) }) describe('AppShell layout renders chrome', () => { it('AppShell renders sidebar and session shell', () => { const { container } = render() expect(container.querySelector('aside')).toBeInTheDocument() expect(container.innerHTML).toContain('New session') expect(container.innerHTML).toContain('Scheduled') expect(container.innerHTML).toContain('All projects') }) }) describe('Design system compliance', () => { it('Pages use Material Symbols Outlined icons', () => { const pages = [EmptySession, AgentTeams, ToolInspection] for (const Page of pages) { const { container, unmount } = render() const icons = container.querySelectorAll('.material-symbols-outlined') expect(icons.length).toBeGreaterThan(0) unmount() } }) it('Current brand color is used in content pages', () => { const pages = [EmptySession] for (const Page of pages) { const { container, unmount } = render() const html = container.innerHTML expect( html.includes('C47A5A') || html.includes('8F482F') || html.includes('var(--color-brand)') || html.includes('bg-[var(--color-brand)]'), ).toBe(true) unmount() } }) }) describe('Mock data integration', () => { it('AgentTeams shows team members from mock data', () => { const { container } = render() expect(container.innerHTML).toContain('Architect') expect(container.innerHTML).toContain('Frontend Dev') expect(container.innerHTML).toContain('Backend Dev') expect(container.innerHTML).toContain('Tester') }) })