test: expand frontend test suite to 82% coverage

Adds ~45 new and updated test files covering Admin, Collab, Dashboard, Map, Memories, PDF, Photos, Planner, Settings, Vacay, Weather components, pages, stores, and a WebSocket integration test.
This commit is contained in:
jubnl
2026-04-08 21:14:23 +02:00
parent 2b7057b922
commit d4bb8be86b
45 changed files with 13643 additions and 524 deletions
@@ -79,4 +79,110 @@ describe('settingsStore', () => {
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-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);
});
});
});
+256
View File
@@ -145,4 +145,260 @@ describe('vacayStore', () => {
expect(state.selectedYear).toBe(2025);
});
});
describe('FE-STORE-VACAY-005: setSelectedYear and setSelectedUserId', () => {
it('updates selectedYear state', () => {
useVacayStore.getState().setSelectedYear(2028);
expect(useVacayStore.getState().selectedYear).toBe(2028);
});
it('updates selectedUserId state', () => {
useVacayStore.getState().setSelectedUserId(42);
expect(useVacayStore.getState().selectedUserId).toBe(42);
});
it('sets selectedUserId to null', () => {
useVacayStore.setState({ selectedUserId: 42 });
useVacayStore.getState().setSelectedUserId(null);
expect(useVacayStore.getState().selectedUserId).toBeNull();
});
});
describe('FE-STORE-VACAY-006: loadEntries() uses selectedYear when no year arg', () => {
it('falls back to selectedYear when called without argument', async () => {
useVacayStore.setState({ selectedYear: 2025 });
await useVacayStore.getState().loadEntries();
expect(useVacayStore.getState().entries.length).toBe(2);
});
});
describe('FE-STORE-VACAY-007: loadStats() uses selectedYear when no year arg', () => {
it('falls back to selectedYear when called without argument', async () => {
useVacayStore.setState({ selectedYear: 2025 });
await useVacayStore.getState().loadStats();
expect(useVacayStore.getState().stats.length).toBe(1);
});
});
describe('FE-STORE-VACAY-008: invite()', () => {
it('calls invite API and reloads plan', async () => {
let inviteCalled = false;
server.use(
http.post('/api/addons/vacay/invite', () => {
inviteCalled = true;
return HttpResponse.json({ success: true });
})
);
await useVacayStore.getState().invite(5);
const state = useVacayStore.getState();
expect(inviteCalled).toBe(true);
expect(state.plan).not.toBeNull();
expect(state.plan?.id).toBe(1);
});
});
describe('FE-STORE-VACAY-009: declineInvite()', () => {
it('calls decline API and reloads plan', async () => {
await useVacayStore.getState().declineInvite(2);
expect(useVacayStore.getState().plan?.id).toBe(1);
});
});
describe('FE-STORE-VACAY-010: cancelInvite()', () => {
it('calls cancel API and reloads plan', async () => {
await useVacayStore.getState().cancelInvite(3);
const state = useVacayStore.getState();
expect(state.plan).not.toBeNull();
expect(state.plan?.id).toBe(1);
});
});
describe('FE-STORE-VACAY-011: acceptInvite()', () => {
it('calls loadAll after accepting invite', async () => {
await useVacayStore.getState().acceptInvite(1);
const state = useVacayStore.getState();
expect(state.plan).not.toBeNull();
expect(state.years).toEqual([2025, 2026]);
expect(state.loading).toBe(false);
});
});
describe('FE-STORE-VACAY-012: dissolve()', () => {
it('calls loadAll after dissolving', async () => {
await useVacayStore.getState().dissolve();
const state = useVacayStore.getState();
expect(state.plan).not.toBeNull();
expect(state.loading).toBe(false);
});
});
describe('FE-STORE-VACAY-013: updateColor()', () => {
it('reloads plan and entries after updating color', async () => {
server.use(
http.put('/api/addons/vacay/color', () =>
HttpResponse.json({ success: true })
)
);
await useVacayStore.getState().updateColor('#ff0000');
const state = useVacayStore.getState();
expect(state.plan?.id).toBe(1);
expect(state.entries.length).toBe(2);
});
});
describe('FE-STORE-VACAY-014: toggleCompanyHoliday()', () => {
it('reloads entries and stats after toggling company holiday', async () => {
useVacayStore.setState({ selectedYear: 2025 });
server.use(
http.post('/api/addons/vacay/entries/company-holiday', () =>
HttpResponse.json({ success: true })
)
);
await useVacayStore.getState().toggleCompanyHoliday('2025-12-26');
const state = useVacayStore.getState();
expect(state.entries.length).toBe(2);
expect(state.stats.length).toBe(1);
});
});
describe('FE-STORE-VACAY-015: updateVacationDays()', () => {
it('reloads stats for the given year', async () => {
await useVacayStore.getState().updateVacationDays(2025, 25);
expect(useVacayStore.getState().stats.length).toBe(1);
});
});
describe('FE-STORE-VACAY-016: removeYear() when selectedYear is not the removed year', () => {
it('does not change selectedYear when a different year is removed', async () => {
useVacayStore.setState({ years: [2025, 2026], selectedYear: 2025 });
await useVacayStore.getState().removeYear(2026);
const state = useVacayStore.getState();
expect(state.years).toEqual([2025]);
expect(state.selectedYear).toBe(2025);
});
});
describe('FE-STORE-VACAY-017: addHolidayCalendar()', () => {
it('reloads plan and holidays after adding a holiday calendar', async () => {
server.use(
http.post('/api/addons/vacay/plan/holiday-calendars', () =>
HttpResponse.json({
calendar: { id: 1, plan_id: 1, region: 'DE', label: null, color: '#ef4444', sort_order: 0 },
})
)
);
await useVacayStore.getState().addHolidayCalendar({ region: 'DE', color: '#ef4444' });
expect(useVacayStore.getState().plan?.id).toBe(1);
});
});
describe('FE-STORE-VACAY-018: updateHolidayCalendar()', () => {
it('reloads plan and holidays after updating a holiday calendar', async () => {
server.use(
http.put('/api/addons/vacay/plan/holiday-calendars/:id', () =>
HttpResponse.json({
calendar: { id: 1, plan_id: 1, region: 'US', label: 'US Holidays', color: '#3b82f6', sort_order: 0 },
})
)
);
await useVacayStore.getState().updateHolidayCalendar(1, { label: 'US Holidays' });
expect(useVacayStore.getState().plan?.id).toBe(1);
});
});
describe('FE-STORE-VACAY-019: deleteHolidayCalendar()', () => {
it('reloads plan and holidays after deleting a holiday calendar', async () => {
await useVacayStore.getState().deleteHolidayCalendar(1);
expect(useVacayStore.getState().plan?.id).toBe(1);
});
});
describe('FE-STORE-VACAY-020: loadHolidays() with regional calendar includes matching counties', () => {
it('includes holidays matching the region county and excludes non-matching ones', async () => {
useVacayStore.setState({
selectedYear: 2025,
plan: {
id: 1,
holidays_enabled: true,
holidays_region: null,
holiday_calendars: [
{ id: 1, plan_id: 1, region: 'DE-BY', label: null, color: '#ef4444', sort_order: 0 },
],
block_weekends: false,
carry_over_enabled: false,
company_holidays_enabled: false,
},
});
server.use(
http.get('/api/addons/vacay/holidays/:year/:country', () =>
HttpResponse.json([
{ date: '2025-11-01', name: 'All Saints Day', localName: 'Allerheiligen', global: false, counties: ['DE-BY', 'DE-BW'] },
{ date: '2025-08-15', name: 'Assumption Day', localName: 'Mariä Himmelfahrt', global: false, counties: ['DE-BY'] },
{ date: '2025-03-19', name: 'St. Joseph', localName: 'Sankt Joseph', global: false, counties: ['DE-NW'] },
])
)
);
await useVacayStore.getState().loadHolidays(2025);
const holidays = useVacayStore.getState().holidays;
// DE-BY holidays should be included
expect(holidays['2025-11-01']).toBeDefined();
expect(holidays['2025-08-15']).toBeDefined();
// DE-NW only holiday should be excluded
expect(holidays['2025-03-19']).toBeUndefined();
});
});
describe('FE-STORE-VACAY-021: loadHolidays() skips regional calendar when data has no county breakdown', () => {
it('results in empty holidays map when all entries are global (no counties)', async () => {
useVacayStore.setState({
selectedYear: 2025,
plan: {
id: 1,
holidays_enabled: true,
holidays_region: null,
holiday_calendars: [
{ id: 1, plan_id: 1, region: 'DE-BY', label: null, color: '#ef4444', sort_order: 0 },
],
block_weekends: false,
carry_over_enabled: false,
company_holidays_enabled: false,
},
});
server.use(
http.get('/api/addons/vacay/holidays/:year/:country', () =>
HttpResponse.json([
{ date: '2025-12-25', name: 'Christmas', localName: 'Weihnachten', global: true, counties: null },
{ date: '2025-01-01', name: 'New Year', localName: 'Neujahr', global: true, counties: null },
])
)
);
await useVacayStore.getState().loadHolidays(2025);
// hasRegions is false (no counties), region is 'DE-BY' (non-null)
// so the condition `hasRegions && !region` is false → proceeds to county filter
// h.global is true → all holidays are included despite region filter
// Actually: global=true entries are included by the `h.global` check in the forEach
// The test verifies behavior when counties: null + global: true
const holidays = useVacayStore.getState().holidays;
// Global holidays are included even for regional calendars when counties data is absent
expect(holidays['2025-12-25']).toBeDefined();
});
});
});