diff --git a/frontend/src/__tests__/fixtures.ts b/frontend/src/__tests__/fixtures.ts new file mode 100644 index 0000000..83c5b09 --- /dev/null +++ b/frontend/src/__tests__/fixtures.ts @@ -0,0 +1,66 @@ +import type { + SynthesisListItem, + Synthesis, + NewsSection, + Source, + UserSettings, + ProviderConfig, + GenerateResponse, +} from '~/types'; +import { DEFAULT_SETTINGS } from '~/types'; + +// ---- Syntheses (list view) ---- +export const MOCK_SYNTHESIS_LIST_ITEM: SynthesisListItem = { + id: 'test-synth-1', + week: '2026-W12', + status: 'completed', + created_at: '2026-03-21T10:00:00Z', + first_section_title: 'Annonces majeures', + first_section_item_count: 3, +}; + +export const MOCK_SYNTHESIS_LIST: SynthesisListItem[] = [ + MOCK_SYNTHESIS_LIST_ITEM, + { ...MOCK_SYNTHESIS_LIST_ITEM, id: 'test-synth-2', week: '2026-W11', first_section_title: null, first_section_item_count: 0 }, +]; + +export const MOCK_SYNTHESIS_IN_PROGRESS: SynthesisListItem = { + ...MOCK_SYNTHESIS_LIST_ITEM, id: 'test-synth-progress', status: 'in_progress', +}; + +// ---- Syntheses (detail view) ---- +export const MOCK_SECTIONS: NewsSection[] = [ + { title: 'Annonces majeures', items: [ + { title: 'GPT-5 Released', url: 'https://example.com/1', summary: 'Summary 1' }, + { title: 'New Chip Launch', url: 'https://example.com/2', summary: 'Summary 2' }, + ]}, +]; + +export const MOCK_SYNTHESIS_DETAIL: Synthesis = { + id: 'test-synth-1', user_id: 'test-user-1', week: '2026-W12', + sections: MOCK_SECTIONS, status: 'completed', created_at: '2026-03-21T10:00:00Z', +}; + +// ---- Sources ---- +export const MOCK_SOURCE: Source = { + id: 'src-1', user_id: 'u1', title: 'Test Blog', url: 'https://test.example.com/blog', created_at: '2026-03-21T10:00:00Z', +}; +export const MOCK_SOURCES: Source[] = [ + MOCK_SOURCE, + { ...MOCK_SOURCE, id: 'src-2', title: 'News Site', url: 'https://news.example.com' }, +]; + +// ---- Settings ---- +export const MOCK_SETTINGS: UserSettings = { + ...DEFAULT_SETTINGS, theme: 'Intelligence Artificielle', ai_provider: 'gemini', ai_model: 'gemini-2.5-pro', ai_model_writing: 'gemini-2.5-flash', +}; + +// ---- Providers ---- +export const MOCK_PROVIDER_CONFIG: ProviderConfig = { + provider_name: 'gemini', display_name: 'Google Gemini', + models: [{ model_id: 'gemini-2.5-pro', display_name: 'Gemini 2.5 Pro' }, { model_id: 'gemini-2.5-flash', display_name: 'Gemini 2.5 Flash' }], +}; +export const MOCK_PROVIDER_CONFIGS: ProviderConfig[] = [MOCK_PROVIDER_CONFIG]; + +// ---- Generate ---- +export const MOCK_GENERATE_RESPONSE: GenerateResponse = { job_id: 'job-test-1' }; diff --git a/frontend/src/__tests__/pages/generate.test.tsx b/frontend/src/__tests__/pages/generate.test.tsx index 9608c56..4da4885 100644 --- a/frontend/src/__tests__/pages/generate.test.tsx +++ b/frontend/src/__tests__/pages/generate.test.tsx @@ -1,8 +1,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { screen, waitFor, fireEvent } from '@solidjs/testing-library'; import { renderWithProviders } from '../test-utils'; -import { DEFAULT_SETTINGS } from '~/types'; -import type { UserSettings, ProviderConfig } from '~/types'; +import { MOCK_SETTINGS, MOCK_PROVIDER_CONFIGS } from '../fixtures'; // Mock API modules vi.mock('~/api/syntheses', () => ({ @@ -46,33 +45,14 @@ const mockedGenerate = vi.mocked(synthesesApi.generate); const mockedProgressUrl = vi.mocked(synthesesApi.progressUrl); const mockedCreateSSE = vi.mocked(createSSEConnection); -const mockSettings: UserSettings = { - ...DEFAULT_SETTINGS, - theme: 'Intelligence Artificielle', - ai_provider: 'gemini', - ai_model: 'gemini-2.5-pro', - ai_model_writing: 'gemini-2.5-flash', -}; - -const mockProviders: ProviderConfig[] = [ - { - provider_name: 'gemini', - display_name: 'Google Gemini', - models: [ - { model_id: 'gemini-2.5-pro', display_name: 'Gemini 2.5 Pro' }, - { model_id: 'gemini-2.5-flash', display_name: 'Gemini 2.5 Flash' }, - ], - }, -]; - afterEach(() => { vi.clearAllMocks(); }); describe('GenerateSynthesis Page', () => { it('should render launch button with settings info', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); renderWithProviders(() => ); @@ -87,8 +67,8 @@ describe('GenerateSynthesis Page', () => { }); it('should call POST /syntheses/generate on launch', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedGenerate.mockResolvedValue({ job_id: 'job-123' }); mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress'); @@ -119,8 +99,8 @@ describe('GenerateSynthesis Page', () => { }); it('should show progress bar from SSE events', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedGenerate.mockResolvedValue({ job_id: 'job-123' }); mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress'); @@ -152,8 +132,8 @@ describe('GenerateSynthesis Page', () => { }); it('should render step checklist with correct states', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedGenerate.mockResolvedValue({ job_id: 'job-123' }); mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress'); @@ -194,8 +174,8 @@ describe('GenerateSynthesis Page', () => { }); it('should show retry button on error', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedGenerate.mockResolvedValue({ job_id: 'job-123' }); mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress'); @@ -227,8 +207,8 @@ describe('GenerateSynthesis Page', () => { }); it('should show completion message on success', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(MOCK_PROVIDER_CONFIGS); mockedGenerate.mockResolvedValue({ job_id: 'job-123' }); mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress'); diff --git a/frontend/src/__tests__/pages/home.test.tsx b/frontend/src/__tests__/pages/home.test.tsx index adbfa24..de5f1c5 100644 --- a/frontend/src/__tests__/pages/home.test.tsx +++ b/frontend/src/__tests__/pages/home.test.tsx @@ -2,7 +2,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { screen, waitFor, fireEvent } from '@solidjs/testing-library'; import { renderWithProviders } from '../test-utils'; import Home from '~/pages/Home'; -import type { SynthesisListItem } from '~/types'; +import { MOCK_SYNTHESIS_LIST, MOCK_SYNTHESIS_IN_PROGRESS } from '../fixtures'; // Mock the syntheses API module vi.mock('~/api/syntheses', () => ({ @@ -21,31 +21,6 @@ import { synthesesApi } from '~/api/syntheses'; const mockedList = vi.mocked(synthesesApi.list); const mockedRemove = vi.mocked(synthesesApi.remove); -const mockSyntheses: SynthesisListItem[] = [ - { - id: 's1', - week: '2026-W12', - status: 'completed', - created_at: '2026-03-21T10:00:00Z', - sections: [ - { - title: 'Annonces majeures', - items: [ - { title: 'GPT-5 Released', url: 'https://example.com/1', summary: 'Summary 1' }, - { title: 'New Chip Launch', url: 'https://example.com/2', summary: 'Summary 2' }, - ], - }, - ], - }, - { - id: 's2', - week: '2026-W11', - status: 'completed', - created_at: '2026-03-14T10:00:00Z', - sections: [], - }, -]; - afterEach(() => { vi.clearAllMocks(); vi.useRealTimers(); @@ -53,7 +28,7 @@ afterEach(() => { describe('Home Page', () => { it('should render synthesis list from mocked API data', async () => { - mockedList.mockResolvedValue(mockSyntheses); + mockedList.mockResolvedValue(MOCK_SYNTHESIS_LIST); renderWithProviders(() => ); @@ -88,7 +63,7 @@ describe('Home Page', () => { }); it('should show "Confirmer" text on first delete click', async () => { - mockedList.mockResolvedValue(mockSyntheses); + mockedList.mockResolvedValue(MOCK_SYNTHESIS_LIST); renderWithProviders(() => ); @@ -105,7 +80,7 @@ describe('Home Page', () => { }); it('should remove item from list on second delete click (confirm)', async () => { - mockedList.mockResolvedValue([...mockSyntheses]); + mockedList.mockResolvedValue([...MOCK_SYNTHESIS_LIST]); mockedRemove.mockResolvedValue(undefined); renderWithProviders(() => ); @@ -127,7 +102,7 @@ describe('Home Page', () => { fireEvent.click(confirmBtn); await waitFor(() => { - expect(mockedRemove).toHaveBeenCalledWith('s1'); + expect(mockedRemove).toHaveBeenCalledWith('test-synth-1'); }); await waitFor(() => { @@ -139,7 +114,7 @@ describe('Home Page', () => { it('should auto-cancel delete confirmation after 3 seconds', async () => { vi.useFakeTimers(); - mockedList.mockResolvedValue(mockSyntheses); + mockedList.mockResolvedValue(MOCK_SYNTHESIS_LIST); renderWithProviders(() => ); @@ -162,17 +137,7 @@ describe('Home Page', () => { }); it('should show in-progress banner when a synthesis has status "in_progress"', async () => { - const inProgressSyntheses: SynthesisListItem[] = [ - { - id: 's-ip', - week: '2026-W12', - status: 'in_progress', - created_at: '2026-03-21T10:00:00Z', - sections: [], - }, - ]; - - mockedList.mockResolvedValue(inProgressSyntheses); + mockedList.mockResolvedValue([MOCK_SYNTHESIS_IN_PROGRESS]); renderWithProviders(() => ); diff --git a/frontend/src/__tests__/pages/settings.test.tsx b/frontend/src/__tests__/pages/settings.test.tsx index 35f0661..1518d2e 100644 --- a/frontend/src/__tests__/pages/settings.test.tsx +++ b/frontend/src/__tests__/pages/settings.test.tsx @@ -2,7 +2,8 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { screen, waitFor, fireEvent } from '@solidjs/testing-library'; import { renderWithProviders } from '../test-utils'; import { DEFAULT_SETTINGS } from '~/types'; -import type { UserSettings, ProviderConfig } from '~/types'; +import type { UserSettings } from '~/types'; +import { MOCK_SETTINGS, MOCK_PROVIDER_CONFIGS } from '../fixtures'; // Mock API modules vi.mock('~/api/settings', () => ({ @@ -38,25 +39,9 @@ const mockedUpdateSettings = vi.mocked(settingsApi.update); const mockedListProviders = vi.mocked(configApi.listProviders); const mockedListKeys = vi.mocked(apiKeysApi.list); -const mockSettings: UserSettings = { - ...DEFAULT_SETTINGS, - theme: 'Intelligence Artificielle', - ai_provider: 'gemini', - ai_model: 'gemini-2.5-pro', - ai_model_writing: 'gemini-2.5-flash', - rate_limit_max_requests: null, - rate_limit_time_window_seconds: null, -}; - -const mockProviders: ProviderConfig[] = [ - { - provider_name: 'gemini', - display_name: 'Google Gemini', - models: [ - { model_id: 'gemini-2.5-pro', display_name: 'Gemini 2.5 Pro' }, - { model_id: 'gemini-2.5-flash', display_name: 'Gemini 2.5 Flash' }, - ], - }, +// Settings page tests need two providers to test switching +const mockProvidersWithOpenAI = [ + ...MOCK_PROVIDER_CONFIGS, { provider_name: 'openai', display_name: 'OpenAI', @@ -73,8 +58,8 @@ afterEach(() => { describe('Settings Page', () => { it('should render form fields populated from API response', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListKeys.mockResolvedValue([]); renderWithProviders(() => ); @@ -88,10 +73,10 @@ describe('Settings Page', () => { }); it('should call PUT /settings when save button is clicked', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListKeys.mockResolvedValue([]); - mockedUpdateSettings.mockResolvedValue(mockSettings); + mockedUpdateSettings.mockResolvedValue(MOCK_SETTINGS); renderWithProviders(() => ); @@ -110,8 +95,8 @@ describe('Settings Page', () => { }); it('should populate provider dropdown from config API', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListKeys.mockResolvedValue([]); renderWithProviders(() => ); @@ -132,8 +117,8 @@ describe('Settings Page', () => { }); it('should update model list when selecting a different provider', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListKeys.mockResolvedValue([]); renderWithProviders(() => ); @@ -162,8 +147,8 @@ describe('Settings Page', () => { }); it('should render two model dropdowns (research + writing)', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListKeys.mockResolvedValue([]); renderWithProviders(() => ); @@ -179,8 +164,8 @@ describe('Settings Page', () => { }); it('should render rate limit inputs (empty when null)', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListKeys.mockResolvedValue([]); renderWithProviders(() => ); @@ -200,12 +185,12 @@ describe('Settings Page', () => { it('should show and clear rate limit values with the reset link', async () => { const settingsWithRateLimits: UserSettings = { - ...mockSettings, + ...MOCK_SETTINGS, rate_limit_max_requests: 10, rate_limit_time_window_seconds: 60, }; mockedGetSettings.mockResolvedValue(settingsWithRateLimits); - mockedListProviders.mockResolvedValue(mockProviders); + mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListKeys.mockResolvedValue([]); renderWithProviders(() => ); @@ -229,8 +214,8 @@ describe('Settings Page', () => { }); it('should trigger download when export button is clicked', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListKeys.mockResolvedValue([]); // Mock URL.createObjectURL and revokeObjectURL @@ -260,8 +245,8 @@ describe('Settings Page', () => { }); it('should populate form from imported JSON file', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListKeys.mockResolvedValue([]); renderWithProviders(() => ); @@ -303,8 +288,8 @@ describe('Settings Page', () => { }); it('should show validation error for empty theme', async () => { - mockedGetSettings.mockResolvedValue(mockSettings); - mockedListProviders.mockResolvedValue(mockProviders); + mockedGetSettings.mockResolvedValue(MOCK_SETTINGS); + mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI); mockedListKeys.mockResolvedValue([]); // Simulate backend returning a validation error mockedUpdateSettings.mockRejectedValue({ diff --git a/frontend/src/__tests__/pages/sources.test.tsx b/frontend/src/__tests__/pages/sources.test.tsx index eac833d..17cdbac 100644 --- a/frontend/src/__tests__/pages/sources.test.tsx +++ b/frontend/src/__tests__/pages/sources.test.tsx @@ -2,6 +2,7 @@ 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', () => ({ @@ -36,21 +37,11 @@ const mockedRemove = vi.mocked(sourcesApi.remove); const mockedBulkImport = vi.mocked(sourcesApi.bulkImport); const mockedExportCsv = vi.mocked(sourcesApi.exportCsv); -const mockSources: Source[] = [ - { - id: 'src1', - user_id: 'u1', - title: 'OpenAI Blog', - url: 'https://openai.com/blog', - created_at: '2026-03-01T10:00:00Z', - }, - { - id: 'src2', - user_id: 'u1', - title: 'Google AI Blog', - url: 'https://ai.googleblog.com', - created_at: '2026-03-02T10:00:00Z', - }, +// 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(() => { @@ -59,7 +50,7 @@ afterEach(() => { describe('Sources Page', () => { it('should render source list from API data', async () => { - mockedList.mockResolvedValue(mockSources); + mockedList.mockResolvedValue(pageSources); renderWithProviders(() => ); @@ -165,8 +156,8 @@ describe('Sources Page', () => { mockedRemove.mockResolvedValue(undefined); // First call returns all sources, second call after delete returns one mockedList - .mockResolvedValueOnce(mockSources) - .mockResolvedValueOnce([mockSources[1]]); + .mockResolvedValueOnce(pageSources) + .mockResolvedValueOnce([pageSources[1]]); renderWithProviders(() => ); @@ -198,7 +189,7 @@ describe('Sources Page', () => { errors: [], }; mockedBulkImport.mockResolvedValue(bulkResponse); - mockedList.mockResolvedValueOnce([]).mockResolvedValueOnce(mockSources); + mockedList.mockResolvedValueOnce([]).mockResolvedValueOnce(pageSources); renderWithProviders(() => ); @@ -229,7 +220,7 @@ describe('Sources Page', () => { }); it('should trigger CSV export download', async () => { - mockedList.mockResolvedValue(mockSources); + mockedList.mockResolvedValue(pageSources); mockedExportCsv.mockResolvedValue(undefined); renderWithProviders(() => );