test(client): expand frontend test suite to 69.1% coverage

Add and extend tests across 32 files (+10 595 lines) covering Admin
panels (AuditLog, Backup, DevNotifications, GitHub), Collab (Chat,
Notes, Panel, Polls), Planner (DayDetailPanel, DayPlanSidebar),
Settings (DisplaySettings, Integrations, MapSettings), Files
(FileManager, FilesPage), Map, Layout (DemoBanner,
InAppNotificationBell), shared pickers (CustomDateTimePicker,
CustomTimePicker), Vacay holidays, pages (Dashboard, Login), unit
stores (authStore, inAppNotificationStore), API (authUrl, client
integration), and i18n. Also updates sonar-project.properties and
MSW trip handlers to support the new cases.
This commit is contained in:
jubnl
2026-04-07 21:55:41 +02:00
parent 9390a2e9c6
commit fd48169219
32 changed files with 10595 additions and 15 deletions
@@ -131,4 +131,221 @@ describe('inAppNotificationStore', () => {
expect(state.unreadCount).toBe(5);
});
});
describe('FE-STORE-NOTIF-007: fetchNotifications early-return when already loading', () => {
it('does not fetch when isLoading is true', async () => {
useInAppNotificationStore.setState({ isLoading: true });
await useInAppNotificationStore.getState().fetchNotifications();
const state = useInAppNotificationStore.getState();
expect(state.notifications).toEqual([]);
expect(state.isLoading).toBe(true);
});
});
describe('FE-STORE-NOTIF-008: fetchNotifications(reset=true) resets existing list', () => {
it('replaces seeded notifications with fresh data', async () => {
// Seed store with 3 notifications
useInAppNotificationStore.setState({
notifications: [
{ ...buildRawNotif({ id: 901 }), title_params: {}, text_params: {}, is_read: false },
{ ...buildRawNotif({ id: 902 }), title_params: {}, text_params: {}, is_read: false },
{ ...buildRawNotif({ id: 903 }), title_params: {}, text_params: {}, is_read: false },
] as never,
total: 3,
});
await useInAppNotificationStore.getState().fetchNotifications(true);
const state = useInAppNotificationStore.getState();
// Should not contain seeded IDs
expect(state.notifications.find(n => n.id === 901)).toBeUndefined();
expect(state.notifications.find(n => n.id === 902)).toBeUndefined();
expect(state.notifications.find(n => n.id === 903)).toBeUndefined();
// Should contain data from MSW (IDs 1-20)
expect(state.notifications.length).toBe(20);
expect(state.isLoading).toBe(false);
});
});
describe('FE-STORE-NOTIF-009: hasMore is set correctly', () => {
it('hasMore is true when more items exist, false when all loaded', async () => {
// Default MSW returns 25 total, 20 per page
await useInAppNotificationStore.getState().fetchNotifications(true);
expect(useInAppNotificationStore.getState().hasMore).toBe(true);
// Second page: offset=20, returns 5 items, total=25 => 25 >= 25 => hasMore=false
await useInAppNotificationStore.getState().fetchNotifications();
expect(useInAppNotificationStore.getState().hasMore).toBe(false);
});
});
describe('FE-STORE-NOTIF-010: fetchUnreadCount updates unreadCount', () => {
it('sets unreadCount from server response', async () => {
useInAppNotificationStore.setState({ unreadCount: 0 });
await useInAppNotificationStore.getState().fetchUnreadCount();
expect(useInAppNotificationStore.getState().unreadCount).toBe(5);
});
});
describe('FE-STORE-NOTIF-011: markUnread(id)', () => {
it('sets is_read to false and increments unreadCount', async () => {
useInAppNotificationStore.setState({
notifications: [{ ...buildRawNotif({ id: 50, is_read: 1 }), title_params: {}, text_params: {}, is_read: true }] as never,
unreadCount: 0,
});
await useInAppNotificationStore.getState().markUnread(50);
const state = useInAppNotificationStore.getState();
expect(state.notifications.find(n => n.id === 50)?.is_read).toBe(false);
expect(state.unreadCount).toBe(1);
});
});
describe('FE-STORE-NOTIF-012: markAllRead()', () => {
it('marks all notifications as read and sets unreadCount to 0', async () => {
useInAppNotificationStore.setState({
notifications: [
{ ...buildRawNotif({ id: 60 }), title_params: {}, text_params: {}, is_read: false },
{ ...buildRawNotif({ id: 61 }), title_params: {}, text_params: {}, is_read: false },
{ ...buildRawNotif({ id: 62 }), title_params: {}, text_params: {}, is_read: false },
] as never,
unreadCount: 3,
});
await useInAppNotificationStore.getState().markAllRead();
const state = useInAppNotificationStore.getState();
expect(state.notifications.every(n => n.is_read === true)).toBe(true);
expect(state.unreadCount).toBe(0);
});
});
describe('FE-STORE-NOTIF-013: deleteNotification removes unread item and decrements counts', () => {
it('removes notification and decrements total and unreadCount', async () => {
useInAppNotificationStore.setState({
notifications: [{ ...buildRawNotif({ id: 5 }), title_params: {}, text_params: {}, is_read: false }] as never,
total: 3,
unreadCount: 1,
});
await useInAppNotificationStore.getState().deleteNotification(5);
const state = useInAppNotificationStore.getState();
expect(state.notifications.find(n => n.id === 5)).toBeUndefined();
expect(state.total).toBe(2);
expect(state.unreadCount).toBe(0);
});
});
describe('FE-STORE-NOTIF-014: deleteNotification on read item does not decrement unreadCount', () => {
it('decrements total but not unreadCount', async () => {
useInAppNotificationStore.setState({
notifications: [{ ...buildRawNotif({ id: 6, is_read: 1 }), title_params: {}, text_params: {}, is_read: true }] as never,
total: 2,
unreadCount: 0,
});
await useInAppNotificationStore.getState().deleteNotification(6);
const state = useInAppNotificationStore.getState();
expect(state.total).toBe(1);
expect(state.unreadCount).toBe(0);
});
});
describe('FE-STORE-NOTIF-015: deleteAll clears all state', () => {
it('resets notifications, total, unreadCount, and hasMore', async () => {
useInAppNotificationStore.setState({
notifications: [
{ ...buildRawNotif({ id: 70 }), title_params: {}, text_params: {}, is_read: false },
{ ...buildRawNotif({ id: 71 }), title_params: {}, text_params: {}, is_read: false },
] as never,
total: 2,
unreadCount: 2,
hasMore: true,
});
await useInAppNotificationStore.getState().deleteAll();
const state = useInAppNotificationStore.getState();
expect(state.notifications).toEqual([]);
expect(state.total).toBe(0);
expect(state.unreadCount).toBe(0);
expect(state.hasMore).toBe(false);
});
});
describe('FE-STORE-NOTIF-016: respondToBoolean updates notification', () => {
it('updates response and is_read from server', async () => {
useInAppNotificationStore.setState({
notifications: [{
...buildRawNotif({ id: 10, type: 'boolean' }),
title_params: {},
text_params: {},
is_read: false,
}] as never,
unreadCount: 1,
});
await useInAppNotificationStore.getState().respondToBoolean(10, 'positive');
const state = useInAppNotificationStore.getState();
const notif = state.notifications.find(n => n.id === 10);
expect(notif?.response).toBe('positive');
expect(notif?.is_read).toBe(true);
});
});
describe('FE-STORE-NOTIF-017: normalizeNotification coerces stringified params', () => {
it('parses JSON string params into objects', () => {
const raw = buildRawNotif({
id: 200,
title_params: '{"trip":"Rome"}',
text_params: '{"user":"alice"}',
});
useInAppNotificationStore.getState().handleNewNotification(raw as never);
const notif = useInAppNotificationStore.getState().notifications.find(n => n.id === 200);
expect(notif?.title_params).toEqual({ trip: 'Rome' });
expect(notif?.text_params).toEqual({ user: 'alice' });
});
});
describe('FE-STORE-NOTIF-018: normalizeNotification handles already-parsed params', () => {
it('stores object params without error', () => {
const raw = buildRawNotif({
id: 201,
title_params: {},
text_params: { key: 'value' },
});
expect(() => {
useInAppNotificationStore.getState().handleNewNotification(raw as never);
}).not.toThrow();
const notif = useInAppNotificationStore.getState().notifications.find(n => n.id === 201);
expect(notif?.title_params).toEqual({});
expect(notif?.text_params).toEqual({ key: 'value' });
});
});
describe('FE-STORE-NOTIF-019: fetchUnreadCount is best-effort', () => {
it('does not throw on server error and preserves state', async () => {
useInAppNotificationStore.setState({ unreadCount: 3 });
server.use(
http.get('/api/notifications/in-app/unread-count', () => {
return new HttpResponse(null, { status: 500 });
}),
);
await expect(useInAppNotificationStore.getState().fetchUnreadCount()).resolves.not.toThrow();
expect(useInAppNotificationStore.getState().unreadCount).toBe(3);
});
});
});