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.
338 lines
10 KiB
TypeScript
338 lines
10 KiB
TypeScript
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(() => <Settings />);
|
|
|
|
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(() => <Settings />);
|
|
|
|
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(() => <Settings />);
|
|
|
|
// The provider names appear in the dropdown (<option>) AND in the ApiKeyManager.
|
|
// Use getAllByText and check that at least one match exists.
|
|
await waitFor(() => {
|
|
expect(screen.getAllByText('Google Gemini').length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
expect(screen.getAllByText('OpenAI').length).toBeGreaterThanOrEqual(1);
|
|
|
|
// Verify the provider select has the options
|
|
const providerSelect = screen.getByLabelText("Fournisseur d'IA") as HTMLSelectElement;
|
|
const options = providerSelect.querySelectorAll('option');
|
|
const optionTexts = Array.from(options).map((o) => o.textContent);
|
|
expect(optionTexts).toContain('Google Gemini');
|
|
expect(optionTexts).toContain('OpenAI');
|
|
});
|
|
|
|
it('should update model list when selecting a different provider', async () => {
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getAllByText('Google Gemini').length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
// Initially Gemini is selected, so its models should be visible in the research model dropdown
|
|
const researchSelect = screen.getByLabelText("Modele d'IA (Recherche et Extraction)") as HTMLSelectElement;
|
|
const geminiOptions = researchSelect.querySelectorAll('option');
|
|
const geminiTexts = Array.from(geminiOptions).map((o) => o.textContent);
|
|
expect(geminiTexts).toContain('Gemini 2.5 Pro');
|
|
|
|
// Change provider to OpenAI
|
|
const providerSelect = screen.getByLabelText(
|
|
"Fournisseur d'IA",
|
|
) as HTMLSelectElement;
|
|
fireEvent.change(providerSelect, { target: { value: 'openai' } });
|
|
|
|
await waitFor(() => {
|
|
const updatedOptions = researchSelect.querySelectorAll('option');
|
|
const updatedTexts = Array.from(updatedOptions).map((o) => o.textContent);
|
|
expect(updatedTexts).toContain('GPT-4o');
|
|
});
|
|
});
|
|
|
|
it('should render two model dropdowns (research + writing)', async () => {
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByLabelText("Modele d'IA (Recherche et Extraction)"),
|
|
).toBeInTheDocument();
|
|
});
|
|
expect(
|
|
screen.getByLabelText("Modele d'IA (Redaction et Synthese)"),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render rate limit inputs (empty when null)', async () => {
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
await waitFor(() => {
|
|
const maxReqInput = screen.getByLabelText(
|
|
'Requetes maximum',
|
|
) as HTMLInputElement;
|
|
expect(maxReqInput.value).toBe('');
|
|
});
|
|
|
|
const timeWindowInput = screen.getByLabelText(
|
|
'Fenetre de temps (secondes)',
|
|
) as HTMLInputElement;
|
|
expect(timeWindowInput.value).toBe('');
|
|
});
|
|
|
|
it('should show and clear rate limit values with the reset link', async () => {
|
|
const settingsWithRateLimits: UserSettings = {
|
|
...mockSettings,
|
|
rate_limit_max_requests: 10,
|
|
rate_limit_time_window_seconds: 60,
|
|
};
|
|
mockedGetSettings.mockResolvedValue(settingsWithRateLimits);
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
await waitFor(() => {
|
|
const maxReqInput = screen.getByLabelText(
|
|
'Requetes maximum',
|
|
) as HTMLInputElement;
|
|
expect(maxReqInput.value).toBe('10');
|
|
});
|
|
|
|
const resetLink = screen.getByText('Reinitialiser');
|
|
fireEvent.click(resetLink);
|
|
|
|
await waitFor(() => {
|
|
const maxReqInput = screen.getByLabelText(
|
|
'Requetes maximum',
|
|
) as HTMLInputElement;
|
|
expect(maxReqInput.value).toBe('');
|
|
});
|
|
});
|
|
|
|
it('should trigger download when export button is clicked', async () => {
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
// Mock URL.createObjectURL and revokeObjectURL
|
|
const mockCreateObjectURL = vi.fn(() => 'blob:test');
|
|
const mockRevokeObjectURL = vi.fn();
|
|
const origCreateObjectURL = URL.createObjectURL;
|
|
const origRevokeObjectURL = URL.revokeObjectURL;
|
|
URL.createObjectURL = mockCreateObjectURL;
|
|
URL.revokeObjectURL = mockRevokeObjectURL;
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTitle('Exporter')).toBeInTheDocument();
|
|
});
|
|
|
|
const exportButton = screen.getByTitle('Exporter');
|
|
fireEvent.click(exportButton);
|
|
|
|
await waitFor(() => {
|
|
expect(mockCreateObjectURL).toHaveBeenCalled();
|
|
});
|
|
|
|
// Restore
|
|
URL.createObjectURL = origCreateObjectURL;
|
|
URL.revokeObjectURL = origRevokeObjectURL;
|
|
});
|
|
|
|
it('should populate form from imported JSON file', async () => {
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTitle('Importer')).toBeInTheDocument();
|
|
});
|
|
|
|
const importedSettings = {
|
|
...DEFAULT_SETTINGS,
|
|
theme: 'Cybersecurite',
|
|
categories: ['Cat A', 'Cat B'],
|
|
};
|
|
|
|
const file = new File(
|
|
[JSON.stringify(importedSettings)],
|
|
'settings.json',
|
|
{ type: 'application/json' },
|
|
);
|
|
|
|
// Find the hidden file input
|
|
const fileInput = document.querySelector(
|
|
'input[type="file"][accept=".json"]',
|
|
) as HTMLInputElement;
|
|
expect(fileInput).toBeTruthy();
|
|
|
|
// Simulate file selection
|
|
Object.defineProperty(fileInput, 'files', {
|
|
value: [file],
|
|
writable: false,
|
|
});
|
|
fireEvent.change(fileInput);
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText("Configuration importee avec succes. N'oubliez pas d'enregistrer."),
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should show validation error for empty theme', async () => {
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
mockedListKeys.mockResolvedValue([]);
|
|
// Simulate backend returning a validation error
|
|
mockedUpdateSettings.mockRejectedValue({
|
|
status: 422,
|
|
message: 'Theme is required',
|
|
});
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByLabelText('Theme de la recherche'),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
// Clear the theme field
|
|
const themeInput = screen.getByLabelText(
|
|
'Theme de la recherche',
|
|
) as HTMLInputElement;
|
|
fireEvent.input(themeInput, { target: { value: '' } });
|
|
|
|
// Click save
|
|
const saveButton = screen.getByText('Enregistrer les parametres');
|
|
fireEvent.click(saveButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Theme is required')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|