/** * RowBindingV2 Tests * ADR-024: Chat & Message Architecture * * Tests for the simplified RowBindingV2 using useAllTables hook */ 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 { RowBindingV2 } from '../RowBindingV2'; // Mock useAllTables hook vi.mock('@/features/tables/hooks/useAllTables', () => ({ useAllTables: vi.fn(() => ({ data: { spacesWithTables: [ { id: 1, name: 'Main Space', icon: '🏠', projects: [ { id: 1, name: 'Project Alpha', icon: '📂', tables: [ { id: '1', name: 'tasks', displayName: 'Tasks', icon: '📋', displayField: 'title', projectId: 1, spaceId: 1 }, { id: '2', name: 'contacts', displayName: 'Contacts', icon: '👤', displayField: 'name', projectId: 1, spaceId: 1 } ] }, { id: 2, name: 'Project Beta', icon: '📁', tables: [ { id: '3', name: 'orders', displayName: 'Orders', icon: '🛒', displayField: 'number', projectId: 2, spaceId: 1 } ] } ] } ], flat: [ { id: '1', name: 'tasks', displayName: 'Tasks', icon: '📋', displayField: 'title', projectId: 1, projectName: 'Project Alpha', spaceId: 1 }, { id: '2', name: 'contacts', displayName: 'Contacts', icon: '👤', displayField: 'name', projectId: 1, projectName: 'Project Alpha', spaceId: 1 }, { id: '3', name: 'orders', displayName: 'Orders', icon: '🛒', displayField: 'number', projectId: 2, projectName: 'Project Beta', spaceId: 1 } ] }, isLoading: false, error: null })) })); // Mock apiClient for rows vi.mock('@/shared/utils/apiClient', () => ({ apiClient: { get: vi.fn().mockImplementation((url: string) => { if (url.includes('/rows')) { return Promise.resolve({ success: true, data: { rows: [ { id: 1, table_id: 1, data: { title: 'Task One', status: 'open' } }, { id: 2, table_id: 1, data: { title: 'Task Two', status: 'done' } }, { id: 3, table_id: 1, data: { title: 'Task Three', status: 'open' } } ] } }); } return Promise.resolve({ success: false }); }) } })); function createTestQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false } } }); } function renderWithProviders(ui: React.ReactElement) { const queryClient = createTestQueryClient(); return render( {ui} ); } describe('RowBindingV2', () => { const mockOnBind = vi.fn(); const mockOnUnbind = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); it('renders header with "Привязать к записи" text', () => { renderWithProviders( ); expect(screen.getByText('Привязать к записи')).toBeInTheDocument(); }); it('expands when clicked', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText('Проект')).toBeInTheDocument(); expect(screen.getByText('Таблица')).toBeInTheDocument(); }); }); it('shows bound rows count badge', () => { const boundRows = [ { table_id: 1, row_id: 1, row_title: 'Task One', table_name: 'Tasks' } ]; renderWithProviders( ); expect(screen.getByText('1')).toBeInTheDocument(); }); it('shows compact bound rows with badges', () => { const boundRows = [ { table_id: 1, row_id: 1, row_title: 'Task One' }, { table_id: 1, row_id: 2, row_title: 'Task Two' } ]; renderWithProviders( ); expect(screen.getByText('Task One')).toBeInTheDocument(); expect(screen.getByText('Task Two')).toBeInTheDocument(); }); it('shows project selector with optgroups after expanding', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { // Check for optgroup labels and options expect(screen.getByText('— Выберите проект —')).toBeInTheDocument(); }); }); it('enables table selector after project selection', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText('Проект')).toBeInTheDocument(); }); // Find project selector by placeholder option const selects = screen.getAllByRole('combobox'); const projectSelect = selects[0]; // First select is project fireEvent.change(projectSelect, { target: { value: '1' } }); await waitFor(() => { const tableSelect = selects[1]; expect(tableSelect).not.toBeDisabled(); }); }); it('shows row search after table selection', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByPlaceholderText('Поиск записей...')).toBeInTheDocument(); }); }); it('shows rows for selected table', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText('Task One')).toBeInTheDocument(); expect(screen.getByText('Task Two')).toBeInTheDocument(); expect(screen.getByText('Task Three')).toBeInTheDocument(); }); }); it('calls onBind when selecting a row', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText('Task One')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Task One')); expect(mockOnBind).toHaveBeenCalledWith( expect.objectContaining({ table_id: 1, row_id: 1, row_title: 'Task One' }) ); }); it('shows collapse button in expanded mode', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText('Свернуть')).toBeInTheDocument(); }); }); it('respects maxBindings limit', async () => { const boundRows = [ { table_id: 1, row_id: 1, row_title: 'Task One' }, { table_id: 1, row_id: 2, row_title: 'Task Two' } ]; renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText(/Достигнут лимит привязок/)).toBeInTheDocument(); }); }); it('shows bound rows list when expanded', async () => { const boundRows = [ { table_id: 1, row_id: 1, row_title: 'Task One', table_name: 'Tasks', table_icon: '📋' } ]; renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText('Привязанные записи (1/10)')).toBeInTheDocument(); expect(screen.getByText('Task One')).toBeInTheDocument(); }); }); it('calls onUnbind when clicking delete on bound row', async () => { const boundRows = [ { table_id: 1, row_id: 1, row_title: 'Task One', table_name: 'Tasks' } ]; renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText('Task One')).toBeInTheDocument(); }); // Find trash button const trashButtons = document.querySelectorAll('button'); const trashButton = Array.from(trashButtons).find(btn => btn.querySelector('svg.lucide-trash-2') ); if (trashButton) { fireEvent.click(trashButton); expect(mockOnUnbind).toHaveBeenCalledWith(1, 1); } }); it('shows green indicator when table is selected', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { // Should show green indicator with table info const greenIndicator = document.querySelector('.bg-green-500\\/10'); expect(greenIndicator).toBeInTheDocument(); }); }); // ADR-024: Auto-mapping to Tasks table describe('Tasks Auto-Mapping', () => { const tasksSource = { tableId: 1, tableName: 'Задачи', tableIcon: '📋', displayColumn: 'title' }; it('shows tasks list immediately when tasksSource is provided', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { // Should show "Quick Tasks" section (emoji and text are separate spans) expect(screen.getByText('Задачи')).toBeInTheDocument(); expect(screen.getByText('📋')).toBeInTheDocument(); }); await waitFor(() => { // Tasks from the table should be visible expect(screen.getByText('Task One')).toBeInTheDocument(); expect(screen.getByText('Task Two')).toBeInTheDocument(); }); }); it('allows selecting a task from quick tasks section', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText('Task One')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Task One')); expect(mockOnBind).toHaveBeenCalledWith( expect.objectContaining({ table_id: 1, row_id: 1, table_name: 'Задачи', table_icon: '📋' }) ); }); it('still shows "Other tables" option when tasksSource is provided', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText('Другая таблица')).toBeInTheDocument(); }); }); }); // ADR-024: Space Files Binding describe('Space Files Binding', () => { it('shows files section when spaceFilesTableId is provided', async () => { renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { // Emoji and text are separate spans expect(screen.getByText('Файлы пространства')).toBeInTheDocument(); }); }); it('allows binding a file from space files table', async () => { // Mock file rows const { apiClient } = await import('@/shared/utils/apiClient'); vi.mocked(apiClient.get).mockImplementation((url: string) => { if (url.includes('/tables/5/rows')) { return Promise.resolve({ success: true, data: { rows: [ { id: 101, table_id: 5, data: { name: 'document.pdf', type: 'application/pdf' } }, { id: 102, table_id: 5, data: { name: 'image.png', type: 'image/png' } } ] } }); } // Default tasks response return Promise.resolve({ success: true, data: { rows: [ { id: 1, table_id: 1, data: { title: 'Task One' } } ] } }); }); renderWithProviders( ); fireEvent.click(screen.getByText('Привязать к записи')); await waitFor(() => { expect(screen.getByText('Файлы пространства')).toBeInTheDocument(); }); // Click on files section to expand fireEvent.click(screen.getByText('Файлы пространства')); await waitFor(() => { expect(screen.getByText('document.pdf')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('document.pdf')); expect(mockOnBind).toHaveBeenCalledWith( expect.objectContaining({ table_id: 5, row_id: 101, row_title: 'document.pdf' }) ); }); }); });