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(() => );