import { describe, it, expect, vi, afterEach } from 'vitest'; import { screen, waitFor, fireEvent } from '@solidjs/testing-library'; import { renderWithProviders } from '../test-utils'; import type { Source, BulkImportResponse } from '~/types'; import { MOCK_SOURCES } from '../fixtures'; // Mock the sources API module vi.mock('~/api/sources', () => ({ sourcesApi: { list: vi.fn(), create: vi.fn(), remove: vi.fn(), bulkImport: vi.fn(), importCsv: vi.fn(), exportCsv: vi.fn(), }, })); // Also mock syntheses since sources imports from it (fetchFile, triggerDownload) vi.mock('~/api/syntheses', () => ({ synthesesApi: { list: vi.fn(), remove: vi.fn(), generate: vi.fn(), progressUrl: vi.fn(), }, fetchFile: vi.fn(), triggerDownload: vi.fn(), })); import { sourcesApi } from '~/api/sources'; import Sources from '~/pages/Sources'; const mockedList = vi.mocked(sourcesApi.list); const mockedCreate = vi.mocked(sourcesApi.create); const mockedRemove = vi.mocked(sourcesApi.remove); const mockedBulkImport = vi.mocked(sourcesApi.bulkImport); const mockedExportCsv = vi.mocked(sourcesApi.exportCsv); // Sources page tests use specific titles that the UI renders, so we define // page-specific sources based on the shared fixture shape. const pageSources: Source[] = [ { ...MOCK_SOURCES[0], id: 'src1', title: 'OpenAI Blog', url: 'https://openai.com/blog', created_at: '2026-03-01T10:00:00Z' }, { ...MOCK_SOURCES[1], id: 'src2', title: 'Google AI Blog', url: 'https://ai.googleblog.com', created_at: '2026-03-02T10:00:00Z' }, ]; afterEach(() => { vi.clearAllMocks(); }); describe('Sources Page', () => { it('should render source list from API data', async () => { mockedList.mockResolvedValue(pageSources); renderWithProviders(() => ); await waitFor(() => { expect(screen.getByText('OpenAI Blog')).toBeInTheDocument(); }); expect(screen.getByText('Google AI Blog')).toBeInTheDocument(); }); it('should show empty state when no sources', async () => { mockedList.mockResolvedValue([]); renderWithProviders(() => ); await waitFor(() => { expect( screen.getByText('Aucune source personnalisee pour le moment.'), ).toBeInTheDocument(); }); }); it('should validate empty title on add form submit', async () => { mockedList.mockResolvedValue([]); renderWithProviders(() => ); await waitFor(() => { expect(screen.getByText('Ajouter')).toBeInTheDocument(); }); // Fill URL but not title const urlInput = screen.getByPlaceholderText('https://...'); fireEvent.input(urlInput, { target: { value: 'https://example.com' } }); const addButton = screen.getByText('Ajouter'); fireEvent.click(addButton); await waitFor(() => { expect(screen.getByText('Le titre est requis.')).toBeInTheDocument(); }); }); it('should validate empty URL on add form submit', async () => { mockedList.mockResolvedValue([]); renderWithProviders(() => ); await waitFor(() => { expect(screen.getByText('Ajouter')).toBeInTheDocument(); }); const titleInput = screen.getByPlaceholderText( 'Nom de la source (ex: Blog de Yann LeCun)', ); fireEvent.input(titleInput, { target: { value: 'My Blog' } }); const addButton = screen.getByText('Ajouter'); fireEvent.click(addButton); await waitFor(() => { expect(screen.getByText("L'URL est requise.")).toBeInTheDocument(); }); }); it('should call API and refresh list when add form is submitted', async () => { const newSource: Source = { id: 'src-new', user_id: 'u1', title: 'New Blog', url: 'https://newblog.com', created_at: '2026-03-20T10:00:00Z', }; mockedCreate.mockResolvedValue(newSource); // First call returns empty, second call after create returns the new source mockedList.mockResolvedValueOnce([]).mockResolvedValueOnce([newSource]); renderWithProviders(() => ); await waitFor(() => { expect(screen.getByText('Ajouter')).toBeInTheDocument(); }); const titleInput = screen.getByPlaceholderText( 'Nom de la source (ex: Blog de Yann LeCun)', ); const urlInput = screen.getByPlaceholderText('https://...'); fireEvent.input(titleInput, { target: { value: 'New Blog' } }); fireEvent.input(urlInput, { target: { value: 'https://newblog.com' } }); const addButton = screen.getByText('Ajouter'); fireEvent.click(addButton); await waitFor(() => { expect(mockedCreate).toHaveBeenCalledWith({ title: 'New Blog', url: 'https://newblog.com', }); }); }); it('should handle delete with confirmation flow', async () => { mockedRemove.mockResolvedValue(undefined); // First call returns all sources, second call after delete returns one mockedList .mockResolvedValueOnce(pageSources) .mockResolvedValueOnce([pageSources[1]]); renderWithProviders(() => ); await waitFor(() => { expect(screen.getByText('OpenAI Blog')).toBeInTheDocument(); }); // First click on delete for first source const deleteButtons = screen.getAllByTitle('Supprimer'); fireEvent.click(deleteButtons[0]); await waitFor(() => { expect(screen.getByText('Confirmer ?')).toBeInTheDocument(); }); // Second click to confirm const confirmBtn = screen.getByText('Confirmer ?'); fireEvent.click(confirmBtn); await waitFor(() => { expect(mockedRemove).toHaveBeenCalledWith('src1'); }); }); it('should trigger bulk import API call', async () => { const bulkResponse: BulkImportResponse = { imported: 2, skipped: 0, errors: [], }; mockedBulkImport.mockResolvedValue(bulkResponse); mockedList.mockResolvedValueOnce([]).mockResolvedValueOnce(pageSources); renderWithProviders(() => ); await waitFor(() => { expect(screen.getByText('Importer les sources')).toBeInTheDocument(); }); // The textarea has id="bulk-import" const textarea = document.getElementById('bulk-import') as HTMLTextAreaElement; expect(textarea).toBeTruthy(); fireEvent.input(textarea, { target: { value: 'OpenAI Blog;https://openai.com/blog\nGoogle AI Blog;https://ai.googleblog.com', }, }); const importButton = screen.getByText('Importer les sources'); fireEvent.click(importButton); await waitFor(() => { expect(mockedBulkImport).toHaveBeenCalledWith({ sources: [ { title: 'OpenAI Blog', url: 'https://openai.com/blog' }, { title: 'Google AI Blog', url: 'https://ai.googleblog.com' }, ], }); }); }); it('should trigger CSV export download', async () => { mockedList.mockResolvedValue(pageSources); mockedExportCsv.mockResolvedValue(undefined); renderWithProviders(() => ); await waitFor(() => { expect(screen.getByText('Exporter en CSV')).toBeInTheDocument(); }); const exportButton = screen.getByText('Exporter en CSV'); fireEvent.click(exportButton); await waitFor(() => { expect(mockedExportCsv).toHaveBeenCalled(); }); }); });