Spaces:
Sleeping
Sleeping
| import { describe, it, expect, beforeEach } from 'vitest'; | |
| import { http, HttpResponse } from 'msw'; | |
| import { server } from '../../helpers/msw/server'; | |
| import { useSettingsStore } from '../../../src/store/settingsStore'; | |
| import { resetAllStores } from '../../helpers/store'; | |
| import { buildSettings } from '../../helpers/factories'; | |
| beforeEach(() => { | |
| resetAllStores(); | |
| }); | |
| describe('settingsStore', () => { | |
| describe('FE-SETTINGS-001: loadSettings()', () => { | |
| it('fetches settings and updates store', async () => { | |
| const settings = buildSettings({ default_currency: 'EUR', language: 'de' }); | |
| server.use( | |
| http.get('/api/settings', () => HttpResponse.json({ settings })) | |
| ); | |
| await useSettingsStore.getState().loadSettings(); | |
| const state = useSettingsStore.getState(); | |
| expect(state.settings.default_currency).toBe('EUR'); | |
| expect(state.settings.language).toBe('de'); | |
| expect(state.isLoaded).toBe(true); | |
| }); | |
| }); | |
| describe('FE-SETTINGS-002: updateSetting() optimistic update', () => { | |
| it('immediately updates local state before API resolves', async () => { | |
| // The store's set() is called synchronously before the first await (settingsApi.set) | |
| // so state is visible without needing to await the full action. | |
| const promise = useSettingsStore.getState().updateSetting('default_currency', 'GBP'); | |
| // Check optimistic state — no await needed here | |
| expect(useSettingsStore.getState().settings.default_currency).toBe('GBP'); | |
| // Let the API call finish to avoid dangling promises | |
| await promise; | |
| }); | |
| }); | |
| describe('FE-SETTINGS-003: updateSetting() reverts on API failure', () => { | |
| it('throws when API fails', async () => { | |
| server.use( | |
| http.put('/api/settings', () => | |
| HttpResponse.json({ error: 'Server error' }, { status: 500 }) | |
| ) | |
| ); | |
| // The store optimistically sets, then throws — the revert is a throw | |
| await expect( | |
| useSettingsStore.getState().updateSetting('default_currency', 'GBP') | |
| ).rejects.toThrow(); | |
| }); | |
| }); | |
| describe('FE-SETTINGS-004: Language change', () => { | |
| it('updates language field and localStorage', async () => { | |
| await useSettingsStore.getState().updateSetting('language', 'fr'); | |
| const state = useSettingsStore.getState(); | |
| expect(state.settings.language).toBe('fr'); | |
| expect(localStorage.getItem('app_language')).toBe('fr'); | |
| }); | |
| }); | |
| describe('FE-SETTINGS-005: loadSettings failure', () => { | |
| it('sets isLoaded: true even on API failure (graceful)', async () => { | |
| server.use( | |
| http.get('/api/settings', () => | |
| HttpResponse.json({ error: 'Server error' }, { status: 500 }) | |
| ) | |
| ); | |
| await useSettingsStore.getState().loadSettings(); | |
| const state = useSettingsStore.getState(); | |
| expect(state.isLoaded).toBe(true); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-006: setLanguageLocal updates state and localStorage', () => { | |
| it('sets language in state and localStorage without an API call', () => { | |
| useSettingsStore.getState().setLanguageLocal('ja'); | |
| const state = useSettingsStore.getState(); | |
| expect(state.settings.language).toBe('ja'); | |
| expect(localStorage.getItem('app_language')).toBe('ja'); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-007: setLanguageLocal without prior localStorage value', () => { | |
| it('writes to localStorage even when no prior value exists', () => { | |
| localStorage.clear(); | |
| useSettingsStore.getState().setLanguageLocal('ko'); | |
| const state = useSettingsStore.getState(); | |
| expect(state.settings.language).toBe('ko'); | |
| expect(localStorage.getItem('app_language')).toBe('ko'); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-008: updateSettings bulk update', () => { | |
| it('updates multiple settings keys and calls bulk API', async () => { | |
| await useSettingsStore.getState().updateSettings({ dark_mode: true, default_currency: 'JPY' }); | |
| const state = useSettingsStore.getState(); | |
| expect(state.settings.dark_mode).toBe(true); | |
| expect(state.settings.default_currency).toBe('JPY'); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-009: updateSettings optimistic update', () => { | |
| it('updates state synchronously before API resolves', async () => { | |
| const promise = useSettingsStore.getState().updateSettings({ dark_mode: true }); | |
| expect(useSettingsStore.getState().settings.dark_mode).toBe(true); | |
| await promise; | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-010: updateSettings API failure throws', () => { | |
| it('throws when bulk API returns 500', async () => { | |
| server.use( | |
| http.post('/api/settings/bulk', () => | |
| HttpResponse.json({ error: 'Server error' }, { status: 500 }) | |
| ) | |
| ); | |
| await expect( | |
| useSettingsStore.getState().updateSettings({ dark_mode: true }) | |
| ).rejects.toThrow(); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-011: updateSetting non-language key does not write to localStorage', () => { | |
| it('does not modify app_language in localStorage', async () => { | |
| const before = localStorage.getItem('app_language'); | |
| await useSettingsStore.getState().updateSetting('dark_mode', true); | |
| expect(localStorage.getItem('app_language')).toBe(before); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-012: loadSettings merges server values with defaults', () => { | |
| it('preserves default keys not returned by server', async () => { | |
| server.use( | |
| http.get('/api/settings', () => | |
| HttpResponse.json({ settings: { dark_mode: true } }) | |
| ) | |
| ); | |
| await useSettingsStore.getState().loadSettings(); | |
| const state = useSettingsStore.getState(); | |
| expect(state.settings.dark_mode).toBe(true); | |
| expect(state.settings.default_currency).toBe('USD'); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-013: updateSetting for time_format', () => { | |
| it('updates time_format in state', async () => { | |
| await useSettingsStore.getState().updateSetting('time_format', '24h'); | |
| expect(useSettingsStore.getState().settings.time_format).toBe('24h'); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-015: setLanguageTransient updates state without touching localStorage', () => { | |
| it('sets language in state but does not write to localStorage', () => { | |
| localStorage.clear(); | |
| useSettingsStore.getState().setLanguageTransient('fr'); | |
| expect(useSettingsStore.getState().settings.language).toBe('fr'); | |
| expect(localStorage.getItem('app_language')).toBeNull(); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-016: setLanguageTransient rejects unsupported language code', () => { | |
| it('leaves state unchanged for an unknown code', () => { | |
| const before = useSettingsStore.getState().settings.language; | |
| useSettingsStore.getState().setLanguageTransient('xx'); | |
| expect(useSettingsStore.getState().settings.language).toBe(before); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-017: setLanguageTransient does not overwrite an explicit localStorage choice', () => { | |
| it('localStorage remains unchanged after a transient set', () => { | |
| localStorage.setItem('app_language', 'de'); | |
| useSettingsStore.getState().setLanguageTransient('es'); | |
| expect(localStorage.getItem('app_language')).toBe('de'); | |
| }); | |
| }); | |
| describe('FE-STORE-SETTINGS-014: updateSetting API failure leaves optimistic state', () => { | |
| it('throws on API failure but keeps the optimistic state', async () => { | |
| server.use( | |
| http.put('/api/settings', () => | |
| HttpResponse.json({ error: 'Server error' }, { status: 500 }) | |
| ) | |
| ); | |
| await expect( | |
| useSettingsStore.getState().updateSetting('default_zoom', 15) | |
| ).rejects.toThrow(); | |
| expect(useSettingsStore.getState().settings.default_zoom).toBe(15); | |
| }); | |
| }); | |
| }); | |