/** * ContactsList Component Tests (v3) * ADR-024: Chat Architecture * TASK-042: Subagents Architecture - correct filter logic * * Key architecture (v3): * - "Контакты" tab: Users with dropdown filter (Все/Люди/AI) * - "AI Чат" tab: AI agents (subagents) from agents prop * - "Задачи" tab: rows from configured table * - Status shown as dot on avatar * - User type shown as text label (Человек/AI Агент) * - Chat history section (collapsible) */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ContactsList } from '../ContactsList.v3'; import { AIAgent } from '../../types'; // Mock apiClient - data must be inline in factory vi.mock('@/shared/utils/apiClient', () => ({ apiClient: { get: vi.fn().mockResolvedValue({ success: true, data: [ { id: 1, name: 'User One', email: 'user1@test.com', user_type: 'human' }, { id: 2, name: 'User Two', email: 'user2@test.com', user_type: 'human' }, { id: 3, name: 'Dev Agent Worker', email: 'dev@ai.local', user_type: 'agent', managed_by_agent_table_id: 10, managed_by_agent_row_id: 42 }, { id: 4, name: 'QA Agent Worker', email: 'qa@ai.local', user_type: 'agent', managed_by_agent_table_id: 10, managed_by_agent_row_id: 43 } ] }) } })); // Subagents (AI agents from table_rows - NOT users) const mockAgents: AIAgent[] = [ { id: 1, name: 'Assistant', icon: '🤖', description: 'General AI assistant', model: 'gpt-4', model_name: 'GPT-4', provider: 'openai', provider_id: 1, system_prompt: 'You are a helpful assistant', is_active: true }, { id: 2, name: 'Translator', icon: '🌐', description: 'Translates text', model: 'gpt-4', model_name: 'GPT-4', provider: 'openai', provider_id: 1, system_prompt: 'You are a translator', is_active: true } ]; function createTestQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false } } }); } function renderWithProviders(ui: React.ReactElement) { const queryClient = createTestQueryClient(); return render( {ui} ); } describe('ContactsList v3', () => { const mockOnSelectAgent = vi.fn(); const mockOnSelectUser = vi.fn(); const mockOnAddToChat = vi.fn(); const mockOnRemoveFromChat = vi.fn(); const mockOnStartChatWithUser = vi.fn(); const mockOnConfigureTasks = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); describe('Tabs', () => { it('renders 3 tabs: Контакты, AI Чат, Задачи', () => { renderWithProviders( ); expect(screen.getByRole('button', { name: /контакты/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /ai чат/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /задачи/i })).toBeInTheDocument(); }); it('Contacts tab is active by default', () => { renderWithProviders( ); const contactsTab = screen.getByRole('button', { name: /контакты/i }); expect(contactsTab).toHaveClass('text-[var(--color-primary-500)]'); }); }); describe('Contacts Tab', () => { it('shows all users by default', async () => { renderWithProviders( ); await waitFor(() => { expect(screen.getByText('User One')).toBeInTheDocument(); expect(screen.getByText('User Two')).toBeInTheDocument(); expect(screen.getByText('Dev Agent Worker')).toBeInTheDocument(); expect(screen.getByText('QA Agent Worker')).toBeInTheDocument(); }); }); it('shows user type labels', async () => { renderWithProviders( ); await waitFor(() => { expect(screen.getByText('User One')).toBeInTheDocument(); }); // Human users should have "Человек" label expect(screen.getAllByText('Человек').length).toBeGreaterThanOrEqual(2); // AI users should have "AI Агент" label expect(screen.getAllByText('AI Агент').length).toBeGreaterThanOrEqual(2); }); it('has filter dropdown to select contact type', async () => { renderWithProviders( ); // Find dropdown with "Все (4)" const filterDropdown = screen.getByRole('combobox'); expect(filterDropdown).toBeInTheDocument(); }); it('filters to humans only when "Люди" selected', async () => { renderWithProviders( ); await waitFor(() => { expect(screen.getByText('User One')).toBeInTheDocument(); }); // Select "Люди" filter const filterDropdown = screen.getByRole('combobox'); fireEvent.change(filterDropdown, { target: { value: 'humans' } }); await waitFor(() => { expect(screen.getByText('User One')).toBeInTheDocument(); expect(screen.getByText('User Two')).toBeInTheDocument(); }); expect(screen.queryByText('Dev Agent Worker')).not.toBeInTheDocument(); expect(screen.queryByText('QA Agent Worker')).not.toBeInTheDocument(); }); it('filters to AI agents only when "AI" selected', async () => { renderWithProviders( ); await waitFor(() => { expect(screen.getByText('Dev Agent Worker')).toBeInTheDocument(); }); // Select "agents" filter const filterDropdown = screen.getByRole('combobox'); fireEvent.change(filterDropdown, { target: { value: 'agents' } }); await waitFor(() => { expect(screen.getByText('Dev Agent Worker')).toBeInTheDocument(); expect(screen.getByText('QA Agent Worker')).toBeInTheDocument(); }); expect(screen.queryByText('User One')).not.toBeInTheDocument(); expect(screen.queryByText('User Two')).not.toBeInTheDocument(); }); it('shows search input for contacts', () => { renderWithProviders( ); expect(screen.getByPlaceholderText(/поиск контактов/i)).toBeInTheDocument(); }); it('filters contacts by search query', async () => { renderWithProviders( ); await waitFor(() => { expect(screen.getByText('User One')).toBeInTheDocument(); }); const searchInput = screen.getByPlaceholderText(/поиск контактов/i); fireEvent.change(searchInput, { target: { value: 'Dev Agent' } }); await waitFor(() => { expect(screen.getByText('Dev Agent Worker')).toBeInTheDocument(); }); expect(screen.queryByText('User One')).not.toBeInTheDocument(); }); it('shows "в чате" badge for participants', async () => { renderWithProviders( ); await waitFor(() => { expect(screen.getByText('User One')).toBeInTheDocument(); }); expect(screen.getByText('в чате')).toBeInTheDocument(); }); it('shows empty state when no contacts match search', async () => { renderWithProviders( ); await waitFor(() => { expect(screen.getByText('User One')).toBeInTheDocument(); }); const searchInput = screen.getByPlaceholderText(/поиск контактов/i); fireEvent.change(searchInput, { target: { value: 'nonexistent' } }); await waitFor(() => { expect(screen.getByText(/ничего не найдено/i)).toBeInTheDocument(); }); }); }); describe('AI Chat Tab', () => { it('displays AI agents when AI Чат tab clicked', async () => { renderWithProviders( ); const aiChatTab = screen.getByRole('button', { name: /ai чат/i }); fireEvent.click(aiChatTab); await waitFor(() => { expect(screen.getByText('Assistant')).toBeInTheDocument(); expect(screen.getByText('Translator')).toBeInTheDocument(); }); }); it('calls onSelectAgent when agent clicked', async () => { renderWithProviders( ); const aiChatTab = screen.getByRole('button', { name: /ai чат/i }); fireEvent.click(aiChatTab); await waitFor(() => { expect(screen.getByText('Assistant')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Assistant')); expect(mockOnSelectAgent).toHaveBeenCalledWith(mockAgents[0]); }); it('highlights current agent with "активен" badge', async () => { renderWithProviders( ); const aiChatTab = screen.getByRole('button', { name: /ai чат/i }); fireEvent.click(aiChatTab); await waitFor(() => { expect(screen.getByText('Assistant')).toBeInTheDocument(); }); expect(screen.getByText('активен')).toBeInTheDocument(); }); it('shows empty state when no agents', () => { renderWithProviders( ); const aiChatTab = screen.getByRole('button', { name: /ai чат/i }); fireEvent.click(aiChatTab); expect(screen.getByText(/нет доступных агентов/i)).toBeInTheDocument(); }); }); describe('Tasks Tab', () => { it('shows setup prompt when no tasksSource configured', () => { renderWithProviders( ); const tasksTab = screen.getByRole('button', { name: /задачи/i }); fireEvent.click(tasksTab); expect(screen.getByText(/источник не настроен/i)).toBeInTheDocument(); }); it('calls onConfigureTasks when configure button clicked', () => { renderWithProviders( ); const tasksTab = screen.getByRole('button', { name: /задачи/i }); fireEvent.click(tasksTab); // Click the "Настроить" button in the empty state const buttons = screen.getAllByRole('button'); const configButton = buttons.find(b => b.textContent?.includes('Настроить')); expect(configButton).toBeTruthy(); if (configButton) { fireEvent.click(configButton); expect(mockOnConfigureTasks).toHaveBeenCalled(); } }); }); describe('Chat History', () => { it('shows chat history section when conversations provided', () => { renderWithProviders( ); expect(screen.getByText('История чатов')).toBeInTheDocument(); }); it('hides chat history when conversations empty', () => { renderWithProviders( ); expect(screen.queryByText('История чатов')).not.toBeInTheDocument(); }); it('displays conversation titles in history', async () => { renderWithProviders( ); // History is shown by default when conversations exist, but collapsed // Click to expand it const historyToggle = screen.getByText('История чатов').closest('button'); expect(historyToggle).toBeInTheDocument(); // Toggle is already expanded when conversations.length > 0, so chats should be visible await waitFor(() => { expect(screen.getByText('Chat 1')).toBeInTheDocument(); expect(screen.getByText('Chat 2')).toBeInTheDocument(); }); }); }); });