mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user