diff --git a/frontend/src/__tests__/pages/generate.test.tsx b/frontend/src/__tests__/pages/generate.test.tsx
new file mode 100644
index 0000000..9608c56
--- /dev/null
+++ b/frontend/src/__tests__/pages/generate.test.tsx
@@ -0,0 +1,262 @@
+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';
+
+// Mock API modules
+vi.mock('~/api/syntheses', () => ({
+ synthesesApi: {
+ list: vi.fn(),
+ generate: vi.fn(),
+ progressUrl: vi.fn(),
+ remove: vi.fn(),
+ },
+ fetchFile: vi.fn(),
+ triggerDownload: vi.fn(),
+}));
+
+vi.mock('~/api/settings', () => ({
+ settingsApi: {
+ get: vi.fn(),
+ update: vi.fn(),
+ },
+}));
+
+vi.mock('~/api/config', () => ({
+ configApi: {
+ listProviders: vi.fn(),
+ },
+}));
+
+// Mock the SSE connection utility
+vi.mock('~/utils/sse', () => ({
+ createSSEConnection: vi.fn(),
+}));
+
+import { synthesesApi } from '~/api/syntheses';
+import { settingsApi } from '~/api/settings';
+import { configApi } from '~/api/config';
+import { createSSEConnection } from '~/utils/sse';
+import GenerateSynthesis from '~/pages/GenerateSynthesis';
+
+const mockedGetSettings = vi.mocked(settingsApi.get);
+const mockedListProviders = vi.mocked(configApi.listProviders);
+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);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Lancer la generation'),
+ ).toBeInTheDocument();
+ });
+
+ expect(screen.getByText(/Google Gemini/)).toBeInTheDocument();
+ expect(screen.getByText(/Gemini 2.5 Pro/)).toBeInTheDocument();
+ });
+
+ it('should call POST /syntheses/generate on launch', async () => {
+ mockedGetSettings.mockResolvedValue(mockSettings);
+ mockedListProviders.mockResolvedValue(mockProviders);
+ mockedGenerate.mockResolvedValue({ job_id: 'job-123' });
+ mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress');
+
+ const mockSSEConnection = {
+ events: vi.fn(() => []),
+ status: vi.fn(() => 'connecting' as const),
+ latestProgress: vi.fn(() => null),
+ completedSynthesisId: vi.fn(() => null),
+ errorMessage: vi.fn(() => null),
+ close: vi.fn(),
+ };
+ mockedCreateSSE.mockReturnValue(mockSSEConnection);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Lancer la generation'),
+ ).toBeInTheDocument();
+ });
+
+ const launchButton = screen.getByText('Lancer la generation');
+ fireEvent.click(launchButton);
+
+ await waitFor(() => {
+ expect(mockedGenerate).toHaveBeenCalled();
+ });
+ });
+
+ it('should show progress bar from SSE events', async () => {
+ mockedGetSettings.mockResolvedValue(mockSettings);
+ mockedListProviders.mockResolvedValue(mockProviders);
+ mockedGenerate.mockResolvedValue({ job_id: 'job-123' });
+ mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress');
+
+ const mockSSEConnection = {
+ events: vi.fn(() => [
+ { type: 'progress' as const, data: { step: 'search', message: 'Recherche...', percent: 25 } },
+ ]),
+ status: vi.fn(() => 'connected' as const),
+ latestProgress: vi.fn(() => ({ step: 'search', message: 'Recherche...', percent: 25 })),
+ completedSynthesisId: vi.fn(() => null),
+ errorMessage: vi.fn(() => null),
+ close: vi.fn(),
+ };
+ mockedCreateSSE.mockReturnValue(mockSSEConnection);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(screen.getByText('Lancer la generation')).toBeInTheDocument();
+ });
+
+ const launchButton = screen.getByText('Lancer la generation');
+ fireEvent.click(launchButton);
+
+ await waitFor(() => {
+ expect(screen.getByText('25%')).toBeInTheDocument();
+ });
+ expect(screen.getByText('Recherche...')).toBeInTheDocument();
+ });
+
+ it('should render step checklist with correct states', async () => {
+ mockedGetSettings.mockResolvedValue(mockSettings);
+ mockedListProviders.mockResolvedValue(mockProviders);
+ mockedGenerate.mockResolvedValue({ job_id: 'job-123' });
+ mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress');
+
+ const mockSSEConnection = {
+ events: vi.fn(() => [
+ { type: 'progress' as const, data: { step: 'search', message: 'Recherche...', percent: 25 } },
+ { type: 'progress' as const, data: { step: 'scraping', message: 'Verification...', percent: 50 } },
+ ]),
+ status: vi.fn(() => 'connected' as const),
+ latestProgress: vi.fn(() => ({ step: 'scraping', message: 'Verification...', percent: 50 })),
+ completedSynthesisId: vi.fn(() => null),
+ errorMessage: vi.fn(() => null),
+ close: vi.fn(),
+ };
+ mockedCreateSSE.mockReturnValue(mockSSEConnection);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(screen.getByText('Lancer la generation')).toBeInTheDocument();
+ });
+
+ const launchButton = screen.getByText('Lancer la generation');
+ fireEvent.click(launchButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Recherche d'actualites"),
+ ).toBeInTheDocument();
+ });
+ expect(
+ screen.getByText('Verification des sources'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('Redaction des resumes'),
+ ).toBeInTheDocument();
+ expect(screen.getByText('Sauvegarde')).toBeInTheDocument();
+ });
+
+ it('should show retry button on error', async () => {
+ mockedGetSettings.mockResolvedValue(mockSettings);
+ mockedListProviders.mockResolvedValue(mockProviders);
+ mockedGenerate.mockResolvedValue({ job_id: 'job-123' });
+ mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress');
+
+ const mockSSEConnection = {
+ events: vi.fn(() => [
+ { type: 'error' as const, message: 'Server error' },
+ ]),
+ status: vi.fn(() => 'error' as const),
+ latestProgress: vi.fn(() => null),
+ completedSynthesisId: vi.fn(() => null),
+ errorMessage: vi.fn(() => 'Server error'),
+ close: vi.fn(),
+ };
+ mockedCreateSSE.mockReturnValue(mockSSEConnection);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(screen.getByText('Lancer la generation')).toBeInTheDocument();
+ });
+
+ const launchButton = screen.getByText('Lancer la generation');
+ fireEvent.click(launchButton);
+
+ await waitFor(() => {
+ expect(screen.getByText('Server error')).toBeInTheDocument();
+ });
+ expect(screen.getByText('Reessayer')).toBeInTheDocument();
+ });
+
+ it('should show completion message on success', async () => {
+ mockedGetSettings.mockResolvedValue(mockSettings);
+ mockedListProviders.mockResolvedValue(mockProviders);
+ mockedGenerate.mockResolvedValue({ job_id: 'job-123' });
+ mockedProgressUrl.mockReturnValue('/api/v1/syntheses/generate/job-123/progress');
+
+ const mockSSEConnection = {
+ events: vi.fn(() => [
+ { type: 'complete' as const, synthesis_id: 'synth-456' },
+ ]),
+ status: vi.fn(() => 'complete' as const),
+ latestProgress: vi.fn(() => ({ step: 'saving', message: 'Done', percent: 100 })),
+ completedSynthesisId: vi.fn(() => 'synth-456'),
+ errorMessage: vi.fn(() => null),
+ close: vi.fn(),
+ };
+ mockedCreateSSE.mockReturnValue(mockSSEConnection);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(screen.getByText('Lancer la generation')).toBeInTheDocument();
+ });
+
+ const launchButton = screen.getByText('Lancer la generation');
+ fireEvent.click(launchButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Synthese generee avec succes ! Redirection...'),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/frontend/src/__tests__/pages/home.test.tsx b/frontend/src/__tests__/pages/home.test.tsx
new file mode 100644
index 0000000..adbfa24
--- /dev/null
+++ b/frontend/src/__tests__/pages/home.test.tsx
@@ -0,0 +1,185 @@
+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';
+
+// Mock the syntheses API module
+vi.mock('~/api/syntheses', () => ({
+ synthesesApi: {
+ list: vi.fn(),
+ remove: vi.fn(),
+ generate: vi.fn(),
+ progressUrl: vi.fn(),
+ },
+ fetchFile: vi.fn(),
+ triggerDownload: vi.fn(),
+}));
+
+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();
+});
+
+describe('Home Page', () => {
+ it('should render synthesis list from mocked API data', async () => {
+ mockedList.mockResolvedValue(mockSyntheses);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(screen.getByText(/Semaine 12/)).toBeInTheDocument();
+ });
+
+ expect(screen.getByText(/Semaine 11/)).toBeInTheDocument();
+ });
+
+ it('should show empty state when list is empty', async () => {
+ mockedList.mockResolvedValue([]);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(screen.getByText('Aucune synthese')).toBeInTheDocument();
+ });
+ });
+
+ it('should have "Nouvelle Synthese" button linking to /generate', async () => {
+ mockedList.mockResolvedValue([]);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(screen.getByText('Nouvelle Synthese')).toBeInTheDocument();
+ });
+
+ const link = screen.getByText('Nouvelle Synthese').closest('a');
+ expect(link).toHaveAttribute('href', '/generate');
+ });
+
+ it('should show "Confirmer" text on first delete click', async () => {
+ mockedList.mockResolvedValue(mockSyntheses);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(screen.getByText(/Semaine 12/)).toBeInTheDocument();
+ });
+
+ const deleteButtons = screen.getAllByTitle('Supprimer');
+ fireEvent.click(deleteButtons[0]);
+
+ await waitFor(() => {
+ expect(screen.getByText('Confirmer')).toBeInTheDocument();
+ });
+ });
+
+ it('should remove item from list on second delete click (confirm)', async () => {
+ mockedList.mockResolvedValue([...mockSyntheses]);
+ mockedRemove.mockResolvedValue(undefined);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(screen.getByText(/Semaine 12/)).toBeInTheDocument();
+ });
+
+ // First click
+ const deleteButtons = screen.getAllByTitle('Supprimer');
+ fireEvent.click(deleteButtons[0]);
+
+ await waitFor(() => {
+ expect(screen.getByText('Confirmer')).toBeInTheDocument();
+ });
+
+ // Second click (confirm)
+ const confirmBtn = screen.getByText('Confirmer').closest('button')!;
+ fireEvent.click(confirmBtn);
+
+ await waitFor(() => {
+ expect(mockedRemove).toHaveBeenCalledWith('s1');
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByText(/Semaine 12/)).not.toBeInTheDocument();
+ });
+ expect(screen.getByText(/Semaine 11/)).toBeInTheDocument();
+ });
+
+ it('should auto-cancel delete confirmation after 3 seconds', async () => {
+ vi.useFakeTimers();
+
+ mockedList.mockResolvedValue(mockSyntheses);
+
+ renderWithProviders(() => );
+
+ await vi.waitFor(() => {
+ expect(screen.getByText(/Semaine 12/)).toBeInTheDocument();
+ });
+
+ const deleteButtons = screen.getAllByTitle('Supprimer');
+ fireEvent.click(deleteButtons[0]);
+
+ await vi.waitFor(() => {
+ expect(screen.getByText('Confirmer')).toBeInTheDocument();
+ });
+
+ vi.advanceTimersByTime(3000);
+
+ await vi.waitFor(() => {
+ expect(screen.queryByText('Confirmer')).not.toBeInTheDocument();
+ });
+ });
+
+ 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);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Une generation est en cours...'),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/frontend/src/__tests__/pages/login.test.tsx b/frontend/src/__tests__/pages/login.test.tsx
new file mode 100644
index 0000000..a2b7681
--- /dev/null
+++ b/frontend/src/__tests__/pages/login.test.tsx
@@ -0,0 +1,125 @@
+import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
+import { screen, waitFor, fireEvent } from '@solidjs/testing-library';
+import { renderWithProviders } from '../test-utils';
+
+// Mock the auth API module
+vi.mock('~/api/auth', () => ({
+ authApi: {
+ login: vi.fn(),
+ register: vi.fn(),
+ me: vi.fn(),
+ logout: vi.fn(),
+ verify: vi.fn(),
+ },
+}));
+
+// Mock the AuthContext so useAuth returns non-authenticated state
+vi.mock('~/contexts/AuthContext', () => ({
+ useAuth: () => ({
+ user: () => null,
+ loading: () => false,
+ isAuthenticated: () => false,
+ isAdmin: () => false,
+ logout: vi.fn(),
+ refreshUser: vi.fn(),
+ }),
+ AuthProvider: (props: any) => props.children,
+}));
+
+import { authApi } from '~/api/auth';
+import Login from '~/pages/Login';
+
+const mockedLogin = vi.mocked(authApi.login);
+
+beforeEach(() => {
+ // Stub Turnstile
+ (window as any).turnstile = {
+ render: vi.fn((_container: HTMLElement, options: any) => {
+ // Immediately invoke callback with a fake token
+ options.callback('fake-turnstile-token');
+ return 'widget-id';
+ }),
+ reset: vi.fn(),
+ remove: vi.fn(),
+ };
+});
+
+afterEach(() => {
+ vi.clearAllMocks();
+ delete (window as any).turnstile;
+});
+
+describe('Login Page', () => {
+ it('should render email input and submit button', async () => {
+ renderWithProviders(() => );
+
+ expect(screen.getByLabelText('Adresse email')).toBeInTheDocument();
+ expect(
+ screen.getByText('Recevoir un lien de connexion'),
+ ).toBeInTheDocument();
+ });
+
+ it('should call POST /auth/login on submit', async () => {
+ mockedLogin.mockResolvedValue({ message: 'OK' });
+
+ renderWithProviders(() => );
+
+ const emailInput = screen.getByLabelText('Adresse email');
+ fireEvent.input(emailInput, {
+ target: { value: 'test@example.com' },
+ });
+
+ const submitButton = screen.getByText('Recevoir un lien de connexion');
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(mockedLogin).toHaveBeenCalledWith({
+ email: 'test@example.com',
+ turnstile_token: 'fake-turnstile-token',
+ });
+ });
+ });
+
+ it('should show "check inbox" message on success', async () => {
+ mockedLogin.mockResolvedValue({ message: 'OK' });
+
+ renderWithProviders(() => );
+
+ const emailInput = screen.getByLabelText('Adresse email');
+ fireEvent.input(emailInput, {
+ target: { value: 'test@example.com' },
+ });
+
+ const submitButton = screen.getByText('Recevoir un lien de connexion');
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Verifiez votre boite de reception'),
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('should show error message on failure', async () => {
+ mockedLogin.mockRejectedValue({
+ status: 400,
+ message: 'Invalid email address',
+ });
+
+ renderWithProviders(() => );
+
+ const emailInput = screen.getByLabelText('Adresse email');
+ fireEvent.input(emailInput, {
+ target: { value: 'test@example.com' },
+ });
+
+ const submitButton = screen.getByText('Recevoir un lien de connexion');
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Invalid email address'),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/frontend/src/__tests__/pages/register.test.tsx b/frontend/src/__tests__/pages/register.test.tsx
new file mode 100644
index 0000000..befa9a3
--- /dev/null
+++ b/frontend/src/__tests__/pages/register.test.tsx
@@ -0,0 +1,130 @@
+import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
+import { screen, waitFor, fireEvent } from '@solidjs/testing-library';
+import { renderWithProviders } from '../test-utils';
+
+// Mock the auth API module
+vi.mock('~/api/auth', () => ({
+ authApi: {
+ login: vi.fn(),
+ register: vi.fn(),
+ me: vi.fn(),
+ logout: vi.fn(),
+ verify: vi.fn(),
+ },
+}));
+
+// Mock the AuthContext so useAuth returns non-authenticated state
+vi.mock('~/contexts/AuthContext', () => ({
+ useAuth: () => ({
+ user: () => null,
+ loading: () => false,
+ isAuthenticated: () => false,
+ isAdmin: () => false,
+ logout: vi.fn(),
+ refreshUser: vi.fn(),
+ }),
+ AuthProvider: (props: any) => props.children,
+}));
+
+import { authApi } from '~/api/auth';
+import Register from '~/pages/Register';
+
+const mockedRegister = vi.mocked(authApi.register);
+
+beforeEach(() => {
+ // Stub Turnstile
+ (window as any).turnstile = {
+ render: vi.fn((_container: HTMLElement, options: any) => {
+ // Immediately invoke callback with a fake token
+ options.callback('fake-turnstile-token');
+ return 'widget-id';
+ }),
+ reset: vi.fn(),
+ remove: vi.fn(),
+ };
+});
+
+afterEach(() => {
+ vi.clearAllMocks();
+ delete (window as any).turnstile;
+});
+
+describe('Register Page', () => {
+ it('should render email, display name inputs and submit button', async () => {
+ renderWithProviders(() => );
+
+ expect(screen.getByLabelText('Adresse email')).toBeInTheDocument();
+ expect(screen.getByLabelText('Nom (optionnel)')).toBeInTheDocument();
+ expect(screen.getByText('Creer mon compte')).toBeInTheDocument();
+ });
+
+ it('should call POST /auth/register on submit', async () => {
+ mockedRegister.mockResolvedValue({ message: 'OK' });
+
+ renderWithProviders(() => );
+
+ const emailInput = screen.getByLabelText('Adresse email');
+ const nameInput = screen.getByLabelText('Nom (optionnel)');
+
+ fireEvent.input(emailInput, {
+ target: { value: 'new@example.com' },
+ });
+ fireEvent.input(nameInput, {
+ target: { value: 'John Doe' },
+ });
+
+ const submitButton = screen.getByText('Creer mon compte');
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(mockedRegister).toHaveBeenCalledWith({
+ email: 'new@example.com',
+ display_name: 'John Doe',
+ turnstile_token: 'fake-turnstile-token',
+ });
+ });
+ });
+
+ it('should show confirmation message on success', async () => {
+ mockedRegister.mockResolvedValue({ message: 'OK' });
+
+ renderWithProviders(() => );
+
+ const emailInput = screen.getByLabelText('Adresse email');
+ fireEvent.input(emailInput, {
+ target: { value: 'new@example.com' },
+ });
+
+ const submitButton = screen.getByText('Creer mon compte');
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Verifiez votre boite de reception'),
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('should show error message on failure', async () => {
+ mockedRegister.mockRejectedValue({
+ status: 422,
+ message: 'Email already exists',
+ });
+
+ renderWithProviders(() => );
+
+ const emailInput = screen.getByLabelText('Adresse email');
+ fireEvent.input(emailInput, {
+ target: { value: 'existing@example.com' },
+ });
+
+ const submitButton = screen.getByText('Creer mon compte');
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Email already exists'),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/frontend/src/__tests__/pages/settings.test.tsx b/frontend/src/__tests__/pages/settings.test.tsx
new file mode 100644
index 0000000..35f0661
--- /dev/null
+++ b/frontend/src/__tests__/pages/settings.test.tsx
@@ -0,0 +1,337 @@
+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';
+
+// Mock API modules
+vi.mock('~/api/settings', () => ({
+ settingsApi: {
+ get: vi.fn(),
+ update: vi.fn(),
+ },
+}));
+
+vi.mock('~/api/config', () => ({
+ configApi: {
+ listProviders: vi.fn(),
+ },
+}));
+
+vi.mock('~/api/apiKeys', () => ({
+ apiKeysApi: {
+ list: vi.fn(),
+ create: vi.fn(),
+ remove: vi.fn(),
+ test: vi.fn(),
+ exportKeys: vi.fn(),
+ },
+}));
+
+import { settingsApi } from '~/api/settings';
+import { configApi } from '~/api/config';
+import { apiKeysApi } from '~/api/apiKeys';
+import Settings from '~/pages/Settings';
+
+const mockedGetSettings = vi.mocked(settingsApi.get);
+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' },
+ ],
+ },
+ {
+ provider_name: 'openai',
+ display_name: 'OpenAI',
+ models: [
+ { model_id: 'gpt-4o', display_name: 'GPT-4o' },
+ { model_id: 'gpt-4o-mini', display_name: 'GPT-4o Mini' },
+ ],
+ },
+];
+
+afterEach(() => {
+ vi.clearAllMocks();
+});
+
+describe('Settings Page', () => {
+ it('should render form fields populated from API response', async () => {
+ mockedGetSettings.mockResolvedValue(mockSettings);
+ mockedListProviders.mockResolvedValue(mockProviders);
+ mockedListKeys.mockResolvedValue([]);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ const themeInput = screen.getByLabelText(
+ 'Theme de la recherche',
+ ) as HTMLInputElement;
+ expect(themeInput.value).toBe('Intelligence Artificielle');
+ });
+ });
+
+ it('should call PUT /settings when save button is clicked', async () => {
+ mockedGetSettings.mockResolvedValue(mockSettings);
+ mockedListProviders.mockResolvedValue(mockProviders);
+ mockedListKeys.mockResolvedValue([]);
+ mockedUpdateSettings.mockResolvedValue(mockSettings);
+
+ renderWithProviders(() => );
+
+ await waitFor(() => {
+ expect(
+ screen.getByLabelText('Theme de la recherche'),
+ ).toBeInTheDocument();
+ });
+
+ const saveButton = screen.getByText('Enregistrer les parametres');
+ fireEvent.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockedUpdateSettings).toHaveBeenCalled();
+ });
+ });
+
+ it('should populate provider dropdown from config API', async () => {
+ mockedGetSettings.mockResolvedValue(mockSettings);
+ mockedListProviders.mockResolvedValue(mockProviders);
+ mockedListKeys.mockResolvedValue([]);
+
+ renderWithProviders(() => );
+
+ // The provider names appear in the dropdown (