/**
* 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();
});
});
});
});