You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
240 lines
7.1 KiB
TypeScript
240 lines
7.1 KiB
TypeScript
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(() => <Sources />);
|
|
|
|
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(() => <Sources />);
|
|
|
|
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(() => <Sources />);
|
|
|
|
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(() => <Sources />);
|
|
|
|
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(() => <Sources />);
|
|
|
|
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(() => <Sources />);
|
|
|
|
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(() => <Sources />);
|
|
|
|
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(() => <Sources />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Exporter en CSV')).toBeInTheDocument();
|
|
});
|
|
|
|
const exportButton = screen.getByText('Exporter en CSV');
|
|
fireEvent.click(exportButton);
|
|
|
|
await waitFor(() => {
|
|
expect(mockedExportCsv).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|