mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
7a22d742ab
Adds new and expanded test suites across client and server to cover the OAuth 2.1 scope system, MCP session manager, collab service, unified memories helpers, OIDC service, budget slice, and OAuth authorize page. Also extends SonarQube coverage exclusions to include bootstrapping files (migrations, scheduler, main.tsx, types.ts) that are not meaningfully testable.
208 lines
7.8 KiB
TypeScript
208 lines
7.8 KiB
TypeScript
// FE-COMP-NOTIF-001 to FE-COMP-NOTIF-016
|
|
import { render, screen, waitFor } from '../../../tests/helpers/render';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { useAuthStore } from '../../store/authStore';
|
|
import { useSettingsStore } from '../../store/settingsStore';
|
|
import { useInAppNotificationStore } from '../../store/inAppNotificationStore';
|
|
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
|
import { buildUser, buildSettings } from '../../../tests/helpers/factories';
|
|
import InAppNotificationItem from './InAppNotificationItem';
|
|
|
|
const buildNotification = (overrides = {}) => ({
|
|
id: 1,
|
|
type: 'simple',
|
|
scope: 'trip',
|
|
target: 1,
|
|
sender_id: 2,
|
|
sender_username: 'alice',
|
|
sender_avatar: null,
|
|
recipient_id: 1,
|
|
title_key: 'notifications.title',
|
|
title_params: '{}',
|
|
text_key: 'notifications.empty',
|
|
text_params: '{}',
|
|
positive_text_key: null,
|
|
negative_text_key: null,
|
|
response: null,
|
|
navigate_text_key: null,
|
|
navigate_target: null,
|
|
is_read: 0,
|
|
created_at: new Date().toISOString(),
|
|
...overrides,
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
|
|
seedStore(useSettingsStore, { settings: buildSettings() });
|
|
});
|
|
|
|
describe('InAppNotificationItem', () => {
|
|
it('FE-COMP-NOTIF-001: renders without crashing', () => {
|
|
render(<InAppNotificationItem notification={buildNotification()} />);
|
|
expect(document.body).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-002: shows sender avatar initial letter', () => {
|
|
render(<InAppNotificationItem notification={buildNotification({ sender_username: 'bob' })} />);
|
|
// Avatar shows first letter uppercase: "B"
|
|
expect(screen.getByText('B')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-003: shows notification title text', () => {
|
|
render(<InAppNotificationItem notification={buildNotification({ title_key: 'notifications.title' })} />);
|
|
// t('notifications.title') = "Notifications"
|
|
expect(screen.getByText('Notifications')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-004: shows notification body text', () => {
|
|
render(<InAppNotificationItem notification={buildNotification({ text_key: 'notifications.empty' })} />);
|
|
// t('notifications.empty') = "No notifications"
|
|
expect(screen.getByText('No notifications')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-005: shows Mark as read button for unread notification', () => {
|
|
render(<InAppNotificationItem notification={buildNotification({ is_read: 0 })} />);
|
|
expect(screen.getByTitle('Mark as read')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-006: does not show Mark as read button for read notification', () => {
|
|
render(<InAppNotificationItem notification={buildNotification({ is_read: 1 })} />);
|
|
expect(screen.queryByTitle('Mark as read')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-007: shows Delete button', () => {
|
|
render(<InAppNotificationItem notification={buildNotification()} />);
|
|
expect(screen.getByTitle('Delete')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-008: clicking Mark as read calls markRead', async () => {
|
|
const user = userEvent.setup();
|
|
const markRead = vi.fn().mockResolvedValue(undefined);
|
|
seedStore(useInAppNotificationStore, { markRead });
|
|
render(<InAppNotificationItem notification={buildNotification({ id: 42, is_read: 0 })} />);
|
|
await user.click(screen.getByTitle('Mark as read'));
|
|
expect(markRead).toHaveBeenCalledWith(42);
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-009: clicking Delete calls deleteNotification', async () => {
|
|
const user = userEvent.setup();
|
|
const deleteNotification = vi.fn().mockResolvedValue(undefined);
|
|
seedStore(useInAppNotificationStore, { deleteNotification });
|
|
render(<InAppNotificationItem notification={buildNotification({ id: 99 })} />);
|
|
await user.click(screen.getByTitle('Delete'));
|
|
expect(deleteNotification).toHaveBeenCalledWith(99);
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-010: shows relative timestamp', () => {
|
|
render(<InAppNotificationItem notification={buildNotification({ created_at: new Date().toISOString() })} />);
|
|
// Recent notification shows "just now"
|
|
expect(screen.getByText('just now')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-011: shows avatar image when sender_avatar is provided', () => {
|
|
render(
|
|
<InAppNotificationItem
|
|
notification={buildNotification({ sender_avatar: 'https://example.com/avatar.png' })}
|
|
/>
|
|
);
|
|
expect(document.querySelector('img')).toBeInTheDocument();
|
|
expect(document.querySelector('img')?.getAttribute('src')).toBe('https://example.com/avatar.png');
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-012: boolean notification shows Accept and Reject buttons', () => {
|
|
render(
|
|
<InAppNotificationItem
|
|
notification={buildNotification({
|
|
type: 'boolean',
|
|
positive_text_key: 'common.yes',
|
|
negative_text_key: 'common.no',
|
|
})}
|
|
/>
|
|
);
|
|
expect(screen.getByText('Yes')).toBeInTheDocument();
|
|
expect(screen.getByText('No')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-013: clicking Accept calls respondToBoolean with positive', async () => {
|
|
const user = userEvent.setup();
|
|
const respondToBoolean = vi.fn().mockResolvedValue(undefined);
|
|
seedStore(useInAppNotificationStore, { respondToBoolean });
|
|
render(
|
|
<InAppNotificationItem
|
|
notification={buildNotification({
|
|
id: 55,
|
|
type: 'boolean',
|
|
positive_text_key: 'common.yes',
|
|
negative_text_key: 'common.no',
|
|
response: null,
|
|
})}
|
|
/>
|
|
);
|
|
await user.click(screen.getByText('Yes'));
|
|
expect(respondToBoolean).toHaveBeenCalledWith(55, 'positive');
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-014: clicking Reject calls respondToBoolean with negative', async () => {
|
|
const user = userEvent.setup();
|
|
const respondToBoolean = vi.fn().mockResolvedValue(undefined);
|
|
seedStore(useInAppNotificationStore, { respondToBoolean });
|
|
render(
|
|
<InAppNotificationItem
|
|
notification={buildNotification({
|
|
id: 66,
|
|
type: 'boolean',
|
|
positive_text_key: 'common.yes',
|
|
negative_text_key: 'common.no',
|
|
response: null,
|
|
})}
|
|
/>
|
|
);
|
|
await user.click(screen.getByText('No'));
|
|
expect(respondToBoolean).toHaveBeenCalledWith(66, 'negative');
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-015: navigate notification shows action button', () => {
|
|
render(
|
|
<InAppNotificationItem
|
|
notification={buildNotification({
|
|
type: 'navigate',
|
|
navigate_text_key: 'notifications.title',
|
|
navigate_target: '/trips/1',
|
|
})}
|
|
/>
|
|
);
|
|
// t('notifications.title') = "Notifications" — the navigate button renders this
|
|
const navigateBtn = document.querySelector('button[style*="pointer"]') ??
|
|
Array.from(document.querySelectorAll('button')).find(b => b.textContent?.includes('Notifications'));
|
|
expect(navigateBtn).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-NOTIF-016: clicking navigate button marks read and navigates', async () => {
|
|
const user = userEvent.setup();
|
|
const markRead = vi.fn().mockResolvedValue(undefined);
|
|
const onClose = vi.fn();
|
|
seedStore(useInAppNotificationStore, { markRead });
|
|
render(
|
|
<InAppNotificationItem
|
|
notification={buildNotification({
|
|
id: 77,
|
|
type: 'navigate',
|
|
navigate_text_key: 'notifications.title',
|
|
navigate_target: '/trips/1',
|
|
is_read: 0,
|
|
})}
|
|
onClose={onClose}
|
|
/>
|
|
);
|
|
// The navigate button renders t('notifications.title') = "Notifications"
|
|
const btn = Array.from(document.querySelectorAll('button')).find(
|
|
b => b.textContent?.includes('Notifications')
|
|
);
|
|
expect(btn).toBeTruthy();
|
|
await user.click(btn!);
|
|
expect(markRead).toHaveBeenCalledWith(77);
|
|
expect(onClose).toHaveBeenCalled();
|
|
});
|
|
});
|