|
|
|
@ -2,7 +2,8 @@ import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
|
|
import { screen, waitFor, fireEvent } from '@solidjs/testing-library';
|
|
|
|
import { screen, waitFor, fireEvent } from '@solidjs/testing-library';
|
|
|
|
import { renderWithProviders } from '../test-utils';
|
|
|
|
import { renderWithProviders } from '../test-utils';
|
|
|
|
import { DEFAULT_SETTINGS } from '~/types';
|
|
|
|
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
|
|
|
|
// Mock API modules
|
|
|
|
vi.mock('~/api/settings', () => ({
|
|
|
|
vi.mock('~/api/settings', () => ({
|
|
|
|
@ -38,25 +39,9 @@ const mockedUpdateSettings = vi.mocked(settingsApi.update);
|
|
|
|
const mockedListProviders = vi.mocked(configApi.listProviders);
|
|
|
|
const mockedListProviders = vi.mocked(configApi.listProviders);
|
|
|
|
const mockedListKeys = vi.mocked(apiKeysApi.list);
|
|
|
|
const mockedListKeys = vi.mocked(apiKeysApi.list);
|
|
|
|
|
|
|
|
|
|
|
|
const mockSettings: UserSettings = {
|
|
|
|
// Settings page tests need two providers to test switching
|
|
|
|
...DEFAULT_SETTINGS,
|
|
|
|
const mockProvidersWithOpenAI = [
|
|
|
|
theme: 'Intelligence Artificielle',
|
|
|
|
...MOCK_PROVIDER_CONFIGS,
|
|
|
|
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',
|
|
|
|
provider_name: 'openai',
|
|
|
|
display_name: 'OpenAI',
|
|
|
|
display_name: 'OpenAI',
|
|
|
|
@ -73,8 +58,8 @@ afterEach(() => {
|
|
|
|
|
|
|
|
|
|
|
|
describe('Settings Page', () => {
|
|
|
|
describe('Settings Page', () => {
|
|
|
|
it('should render form fields populated from API response', async () => {
|
|
|
|
it('should render form fields populated from API response', async () => {
|
|
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
|
|
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
@ -88,10 +73,10 @@ describe('Settings Page', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should call PUT /settings when save button is clicked', async () => {
|
|
|
|
it('should call PUT /settings when save button is clicked', async () => {
|
|
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
|
|
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedUpdateSettings.mockResolvedValue(mockSettings);
|
|
|
|
mockedUpdateSettings.mockResolvedValue(MOCK_SETTINGS);
|
|
|
|
|
|
|
|
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
|
|
|
|
|
|
|
|
@ -110,8 +95,8 @@ describe('Settings Page', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should populate provider dropdown from config API', async () => {
|
|
|
|
it('should populate provider dropdown from config API', async () => {
|
|
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
|
|
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
@ -132,8 +117,8 @@ describe('Settings Page', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should update model list when selecting a different provider', async () => {
|
|
|
|
it('should update model list when selecting a different provider', async () => {
|
|
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
|
|
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
@ -162,8 +147,8 @@ describe('Settings Page', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should render two model dropdowns (research + writing)', async () => {
|
|
|
|
it('should render two model dropdowns (research + writing)', async () => {
|
|
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
|
|
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
@ -179,8 +164,8 @@ describe('Settings Page', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should render rate limit inputs (empty when null)', async () => {
|
|
|
|
it('should render rate limit inputs (empty when null)', async () => {
|
|
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
|
|
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
@ -200,12 +185,12 @@ describe('Settings Page', () => {
|
|
|
|
|
|
|
|
|
|
|
|
it('should show and clear rate limit values with the reset link', async () => {
|
|
|
|
it('should show and clear rate limit values with the reset link', async () => {
|
|
|
|
const settingsWithRateLimits: UserSettings = {
|
|
|
|
const settingsWithRateLimits: UserSettings = {
|
|
|
|
...mockSettings,
|
|
|
|
...MOCK_SETTINGS,
|
|
|
|
rate_limit_max_requests: 10,
|
|
|
|
rate_limit_max_requests: 10,
|
|
|
|
rate_limit_time_window_seconds: 60,
|
|
|
|
rate_limit_time_window_seconds: 60,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
mockedGetSettings.mockResolvedValue(settingsWithRateLimits);
|
|
|
|
mockedGetSettings.mockResolvedValue(settingsWithRateLimits);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
@ -229,8 +214,8 @@ describe('Settings Page', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should trigger download when export button is clicked', async () => {
|
|
|
|
it('should trigger download when export button is clicked', async () => {
|
|
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
|
|
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
// Mock URL.createObjectURL and revokeObjectURL
|
|
|
|
// Mock URL.createObjectURL and revokeObjectURL
|
|
|
|
@ -260,8 +245,8 @@ describe('Settings Page', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should populate form from imported JSON file', async () => {
|
|
|
|
it('should populate form from imported JSON file', async () => {
|
|
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
|
|
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
renderWithProviders(() => <Settings />);
|
|
|
|
@ -303,8 +288,8 @@ describe('Settings Page', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should show validation error for empty theme', async () => {
|
|
|
|
it('should show validation error for empty theme', async () => {
|
|
|
|
mockedGetSettings.mockResolvedValue(mockSettings);
|
|
|
|
mockedGetSettings.mockResolvedValue(MOCK_SETTINGS);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProviders);
|
|
|
|
mockedListProviders.mockResolvedValue(mockProvidersWithOpenAI);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
mockedListKeys.mockResolvedValue([]);
|
|
|
|
// Simulate backend returning a validation error
|
|
|
|
// Simulate backend returning a validation error
|
|
|
|
mockedUpdateSettings.mockRejectedValue({
|
|
|
|
mockedUpdateSettings.mockRejectedValue({
|
|
|
|
|