fix: update failing frontend tests for multi-theme migration

Settings tests now reference fields that exist in the current UI
(max articles per source instead of removed theme field). Generate
tests now mock the themes API and use current pipeline step names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
master
oabrivard 3 months ago
parent b30d04c6c5
commit c51a99051f

@ -28,6 +28,15 @@ vi.mock('~/api/config', () => ({
}, },
})); }));
vi.mock('~/api/themes', () => ({
themesApi: {
list: vi.fn(),
create: vi.fn(),
update: vi.fn(),
remove: vi.fn(),
},
}));
// Mock the SSE connection utility // Mock the SSE connection utility
vi.mock('~/utils/sse', () => ({ vi.mock('~/utils/sse', () => ({
createSSEConnection: vi.fn(), createSSEConnection: vi.fn(),
@ -36,6 +45,7 @@ vi.mock('~/utils/sse', () => ({
import { synthesesApi } from '~/api/syntheses'; import { synthesesApi } from '~/api/syntheses';
import { settingsApi } from '~/api/settings'; import { settingsApi } from '~/api/settings';
import { configApi } from '~/api/config'; import { configApi } from '~/api/config';
import { themesApi } from '~/api/themes';
import { createSSEConnection } from '~/utils/sse'; import { createSSEConnection } from '~/utils/sse';
import GenerateSynthesis from '~/pages/GenerateSynthesis'; import GenerateSynthesis from '~/pages/GenerateSynthesis';
@ -44,6 +54,11 @@ const mockedListProviders = vi.mocked(configApi.listProviders);
const mockedGenerate = vi.mocked(synthesesApi.generate); const mockedGenerate = vi.mocked(synthesesApi.generate);
const mockedProgressUrl = vi.mocked(synthesesApi.progressUrl); const mockedProgressUrl = vi.mocked(synthesesApi.progressUrl);
const mockedCreateSSE = vi.mocked(createSSEConnection); const mockedCreateSSE = vi.mocked(createSSEConnection);
const mockedListThemes = vi.mocked(themesApi.list);
const MOCK_THEMES = [
{ id: 'theme-1', name: 'IA', theme: 'Intelligence Artificielle', categories: ['Cat1'], max_items_per_category: 5, max_age_days: 7, summary_length: 2, created_at: '2026-03-01T00:00:00Z', updated_at: '2026-03-01T00:00:00Z' },
];
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@ -53,6 +68,7 @@ describe('GenerateSynthesis Page', () => {
it('should render launch button with settings info', async () => { it('should render launch button with settings info', async () => {
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS);
mockedListThemes.mockResolvedValue(MOCK_THEMES);
renderWithProviders(() => <GenerateSynthesis />); renderWithProviders(() => <GenerateSynthesis />);
@ -69,6 +85,7 @@ describe('GenerateSynthesis Page', () => {
it('should call POST /syntheses/generate on launch', async () => { it('should call POST /syntheses/generate on launch', async () => {
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS);
mockedListThemes.mockResolvedValue(MOCK_THEMES);
mockedGenerate.mockResolvedValue({ job_id: 'job-123' }); mockedGenerate.mockResolvedValue({ job_id: 'job-123' });
mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress'); mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress');
@ -101,15 +118,16 @@ describe('GenerateSynthesis Page', () => {
it('should show progress bar from SSE events', async () => { it('should show progress bar from SSE events', async () => {
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS);
mockedListThemes.mockResolvedValue(MOCK_THEMES);
mockedGenerate.mockResolvedValue({ job_id: 'job-123' }); mockedGenerate.mockResolvedValue({ job_id: 'job-123' });
mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress'); mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress');
const mockSSEConnection = { const mockSSEConnection = {
events: vi.fn(() => [ events: vi.fn(() => [
{ type: 'progress' as const, data: { step: 'search', message: 'Recherche...', percent: 25 } }, { type: 'progress' as const, data: { step: 'sources', message: 'Analyse des sources...', percent: 25 } },
]), ]),
status: vi.fn(() => 'connected' as const), status: vi.fn(() => 'connected' as const),
latestProgress: vi.fn(() => ({ step: 'search', message: 'Recherche...', percent: 25 })), latestProgress: vi.fn(() => ({ step: 'sources', message: 'Analyse des sources...', percent: 25 })),
completedSynthesisId: vi.fn(() => null), completedSynthesisId: vi.fn(() => null),
errorMessage: vi.fn(() => null), errorMessage: vi.fn(() => null),
close: vi.fn(), close: vi.fn(),
@ -128,22 +146,23 @@ describe('GenerateSynthesis Page', () => {
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('25%')).toBeInTheDocument(); expect(screen.getByText('25%')).toBeInTheDocument();
}); });
expect(screen.getByText('Recherche...')).toBeInTheDocument(); expect(screen.getByText('Analyse des sources...')).toBeInTheDocument();
}); });
it('should render step checklist with correct states', async () => { it('should render step checklist with correct states', async () => {
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS);
mockedListThemes.mockResolvedValue(MOCK_THEMES);
mockedGenerate.mockResolvedValue({ job_id: 'job-123' }); mockedGenerate.mockResolvedValue({ job_id: 'job-123' });
mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress'); mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress');
const mockSSEConnection = { const mockSSEConnection = {
events: vi.fn(() => [ events: vi.fn(() => [
{ type: 'progress' as const, data: { step: 'search', message: 'Recherche...', percent: 25 } }, { type: 'progress' as const, data: { step: 'sources', message: 'Analyse...', percent: 25 } },
{ type: 'progress' as const, data: { step: 'scraping', message: 'Verification...', percent: 50 } }, { type: 'progress' as const, data: { step: 'websearch', message: 'Recherche web...', percent: 50 } },
]), ]),
status: vi.fn(() => 'connected' as const), status: vi.fn(() => 'connected' as const),
latestProgress: vi.fn(() => ({ step: 'scraping', message: 'Verification...', percent: 50 })), latestProgress: vi.fn(() => ({ step: 'websearch', message: 'Recherche web...', percent: 50 })),
completedSynthesisId: vi.fn(() => null), completedSynthesisId: vi.fn(() => null),
errorMessage: vi.fn(() => null), errorMessage: vi.fn(() => null),
close: vi.fn(), close: vi.fn(),
@ -161,14 +180,11 @@ describe('GenerateSynthesis Page', () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByText("Recherche d'actualites"), screen.getByText('Sources personnalisees'),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
expect( expect(
screen.getByText('Verification des sources'), screen.getByText('Recherche web'),
).toBeInTheDocument();
expect(
screen.getByText('Redaction des resumes'),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByText('Sauvegarde')).toBeInTheDocument(); expect(screen.getByText('Sauvegarde')).toBeInTheDocument();
}); });
@ -176,6 +192,7 @@ describe('GenerateSynthesis Page', () => {
it('should show retry button on error', async () => { it('should show retry button on error', async () => {
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS);
mockedListThemes.mockResolvedValue(MOCK_THEMES);
mockedGenerate.mockResolvedValue({ job_id: 'job-123' }); mockedGenerate.mockResolvedValue({ job_id: 'job-123' });
mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress'); mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress');
@ -209,6 +226,7 @@ describe('GenerateSynthesis Page', () => {
it('should show completion message on success', async () => { it('should show completion message on success', async () => {
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS);
mockedListThemes.mockResolvedValue(MOCK_THEMES);
mockedGenerate.mockResolvedValue({ job_id: 'job-123' }); mockedGenerate.mockResolvedValue({ job_id: 'job-123' });
mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress'); mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress');

@ -69,10 +69,10 @@ describe('Settings Page', () => {
renderWithProviders(() => <Settings />); renderWithProviders(() => <Settings />);
await waitFor(() => { await waitFor(() => {
const themeInput = screen.getByLabelText( const maxArticlesInput = screen.getByLabelText(
'Theme de la recherche', 'Articles max par source',
) as HTMLInputElement; ) as HTMLInputElement;
expect(themeInput.value).toBe('Intelligence Artificielle'); expect(maxArticlesInput.value).toBe('3');
}); });
}); });
@ -86,7 +86,7 @@ describe('Settings Page', () => {
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByLabelText('Theme de la recherche'), screen.getByLabelText('Articles max par source'),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@ -113,7 +113,8 @@ describe('Settings Page', () => {
expect(screen.getAllByText('OpenAI').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('OpenAI').length).toBeGreaterThanOrEqual(1);
// Verify the provider select has the options // Verify the provider select has the options
const providerSelect = screen.getByLabelText("Fournisseur d'IA") as HTMLSelectElement; const providerSelect = document.getElementById('aiProvider') as HTMLSelectElement;
expect(providerSelect).toBeTruthy();
const options = providerSelect.querySelectorAll('option'); const options = providerSelect.querySelectorAll('option');
const optionTexts = Array.from(options).map((o) => o.textContent); const optionTexts = Array.from(options).map((o) => o.textContent);
expect(optionTexts).toContain('Google Gemini'); expect(optionTexts).toContain('Google Gemini');
@ -138,9 +139,7 @@ describe('Settings Page', () => {
expect(geminiTexts).toContain('Gemini 2.5 Pro'); expect(geminiTexts).toContain('Gemini 2.5 Pro');
// Change provider to OpenAI // Change provider to OpenAI
const providerSelect = screen.getByLabelText( const providerSelect = document.getElementById('aiProvider') as HTMLSelectElement;
"Fournisseur d'IA",
) as HTMLSelectElement;
fireEvent.change(providerSelect, { target: { value: 'openai' } }); fireEvent.change(providerSelect, { target: { value: 'openai' } });
await waitFor(() => { await waitFor(() => {
@ -291,36 +290,30 @@ describe('Settings Page', () => {
}); });
}); });
it('should show validation error for empty theme', async () => { it('should show validation error from backend on save', async () => {
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
mockedListKeys.mockResolvedValue([]); mockedListKeys.mockResolvedValue([]);
// Simulate backend returning a validation error // Simulate backend returning a validation error
mockedUpdateSettings.mockRejectedValue({ mockedUpdateSettings.mockRejectedValue({
status: 422, status: 422,
message: 'Theme is required', message: 'Validation error',
}); });
renderWithProviders(() => <Settings />); renderWithProviders(() => <Settings />);
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getByLabelText('Theme de la recherche'), screen.getByLabelText('Articles max par source'),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
// Clear the theme field
const themeInput = screen.getByLabelText(
'Theme de la recherche',
) as HTMLInputElement;
fireEvent.input(themeInput, { target: { value: '' } });
// Click save // Click save
const saveButton = screen.getByText('Enregistrer les parametres'); const saveButton = screen.getByText('Enregistrer les parametres');
fireEvent.click(saveButton); fireEvent.click(saveButton);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Theme is required')).toBeInTheDocument(); expect(screen.getByText('Validation error')).toBeInTheDocument();
}); });
}); });
}); });

Loading…
Cancel
Save