mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
31859651d7
Every help screen checked against the v5 redesign. Tours is documented for the first time, as a screen of its own with twelve screens under it, and notifications get help of their own. All pictures are taken again on a realistic demo trip, with short loops where a still cannot show the motion. The English is plain and every language is written naturally rather than word for word. The switcher shows two levels at most, a screen's overview lists its sub-screens as cards above its guides, each wiki page is linked once, and the help sidebar is wider. Fixes found along the way: Save to Collection keeps a tour's route, a track dropped on the places column opens the Tours import, tour Share on desktop, the unsaved tour draft lives on the account, a route click no longer adds an end point, Trip places leaves the trip's tours out, Escape closes a stage's day menu, confirms before destructive deletes, DWD warnings sharing one id kept apart, a booking's date moves with it, and smaller fixes across notifications, bookings, the day plan, stays, Atlas, Collections, Vacay and the admin screens.
208 lines
7.5 KiB
TypeScript
208 lines
7.5 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { render, screen, waitFor } from '../../tests/helpers/render';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { http, HttpResponse } from 'msw';
|
|
import { server } from '../../tests/helpers/msw/server';
|
|
import { resetAllStores, seedStore } from '../../tests/helpers/store';
|
|
import { buildUser } from '../../tests/helpers/factories';
|
|
import { useAuthStore } from '../store/authStore';
|
|
import { useInAppNotificationStore } from '../store/inAppNotificationStore';
|
|
import InAppNotificationsPage from './InAppNotificationsPage';
|
|
|
|
// Mock InAppNotificationItem to simplify rendering
|
|
vi.mock('../components/Notifications/InAppNotificationItem', () => ({
|
|
default: ({ notification }: { notification: { id: number; is_read: number } }) => (
|
|
<div
|
|
data-testid={`notification-${notification.id}`}
|
|
data-read={notification.is_read}
|
|
>
|
|
Notification {notification.id}
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
seedStore(useAuthStore, { isAuthenticated: true, user: buildUser() });
|
|
});
|
|
|
|
describe('InAppNotificationsPage', () => {
|
|
describe('FE-PAGE-NOTIFPAGE-001: Notification list loads on mount', () => {
|
|
it('fetches and displays notifications on mount', async () => {
|
|
render(<InAppNotificationsPage />);
|
|
|
|
// Default handler returns 20 notifications (offset 0..19 from 25 total)
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('notification-1')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-NOTIFPAGE-002: Unread notifications shown with indicator', () => {
|
|
it('shows unread count badge when there are unread notifications', async () => {
|
|
render(<InAppNotificationsPage />);
|
|
|
|
// Default handler returns unread_count: 5
|
|
// The badge shows the count as a span inside the heading
|
|
await waitFor(() => {
|
|
// The "5" badge appears next to the Notifications heading
|
|
const badges = screen.getAllByText('5');
|
|
expect(badges.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-NOTIFPAGE-003: Mark all read button', () => {
|
|
it('shows "Mark all read" button when there are unread notifications', async () => {
|
|
render(<InAppNotificationsPage />);
|
|
|
|
await waitFor(() => {
|
|
// Button has "Mark all read" text (possibly hidden on mobile via CSS class)
|
|
// In jsdom, CSS "hidden" class doesn't actually hide elements
|
|
expect(screen.getByRole('button', { name: /mark all read/i })).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-NOTIFPAGE-004: Delete all button', () => {
|
|
it('shows "Delete all" button when there are notifications', async () => {
|
|
render(<InAppNotificationsPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /delete all/i })).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-NOTIFPAGE-004b: Delete all asks first', () => {
|
|
it('opens a confirmation and only deletes after confirming', async () => {
|
|
const user = userEvent.setup();
|
|
const deleteAll = vi.fn(async () => {});
|
|
render(<InAppNotificationsPage />);
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /delete all/i })).toBeInTheDocument();
|
|
});
|
|
seedStore(useInAppNotificationStore, { deleteAll });
|
|
|
|
await user.click(screen.getByRole('button', { name: /delete all/i }));
|
|
expect(deleteAll).not.toHaveBeenCalled();
|
|
expect(screen.getByText('Delete all notifications?')).toBeInTheDocument();
|
|
|
|
await user.click(screen.getAllByRole('button', { name: /delete all/i }).slice(-1)[0]);
|
|
expect(deleteAll).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-NOTIFPAGE-005: Empty state when no notifications', () => {
|
|
it('shows empty state when API returns no notifications', async () => {
|
|
server.use(
|
|
http.get('/api/notifications/in-app', () => {
|
|
return HttpResponse.json({
|
|
notifications: [],
|
|
total: 0,
|
|
unread_count: 0,
|
|
});
|
|
}),
|
|
);
|
|
|
|
render(<InAppNotificationsPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/no notifications/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-NOTIFPAGE-006: Filter toggle', () => {
|
|
it('renders "All" and "Unread" filter buttons', async () => {
|
|
render(<InAppNotificationsPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /^all$/i })).toBeInTheDocument();
|
|
});
|
|
|
|
// The unread filter button uses t('notifications.unreadOnly') = 'Unread'
|
|
expect(screen.getByRole('button', { name: /^unread$/i })).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-NOTIFPAGE-007: Unread only filter hides read notifications', () => {
|
|
it('clicking Unread filter shows only unread notifications', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
// Seed store with known mix of read/unread
|
|
const unreadNotif = {
|
|
id: 100, is_read: 0, type: 'simple',
|
|
scope: 'trip', target: 1, sender_id: 2,
|
|
sender_username: 'alice', sender_avatar: null,
|
|
recipient_id: 1, title_key: 'n', title_params: '{}',
|
|
text_key: 'n', text_params: '{}',
|
|
positive_text_key: null, negative_text_key: null,
|
|
response: null, navigate_text_key: null, navigate_target: null,
|
|
created_at: '2025-01-01T00:00:00Z',
|
|
};
|
|
const readNotif = {
|
|
id: 101, is_read: 1, type: 'simple',
|
|
scope: 'trip', target: 1, sender_id: 2,
|
|
sender_username: 'alice', sender_avatar: null,
|
|
recipient_id: 1, title_key: 'n', title_params: '{}',
|
|
text_key: 'n', text_params: '{}',
|
|
positive_text_key: null, negative_text_key: null,
|
|
response: null, navigate_text_key: null, navigate_target: null,
|
|
created_at: '2025-01-01T00:00:00Z',
|
|
};
|
|
|
|
seedStore(useInAppNotificationStore, {
|
|
notifications: [unreadNotif, readNotif],
|
|
unreadCount: 1,
|
|
total: 2,
|
|
isLoading: false,
|
|
hasMore: false,
|
|
fetchNotifications: vi.fn(),
|
|
markAllRead: vi.fn(),
|
|
deleteAll: vi.fn(),
|
|
} as any);
|
|
|
|
render(<InAppNotificationsPage />);
|
|
|
|
// Both notifications start visible
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('notification-100')).toBeInTheDocument();
|
|
expect(screen.getByTestId('notification-101')).toBeInTheDocument();
|
|
});
|
|
|
|
// Click "Unread" filter
|
|
await user.click(screen.getByRole('button', { name: /^unread$/i }));
|
|
|
|
// Only unread notification should be visible
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('notification-100')).toBeInTheDocument();
|
|
expect(screen.queryByTestId('notification-101')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-NOTIFPAGE-008: Page title', () => {
|
|
it('shows "Notifications" heading', async () => {
|
|
render(<InAppNotificationsPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
|
});
|
|
|
|
expect(screen.getByRole('heading', { level: 1 }).textContent).toMatch(/notifications/i);
|
|
});
|
|
});
|
|
|
|
describe('FE-PAGE-NOTIFPAGE-009: Notification total count', () => {
|
|
it('shows total notification count in the subtitle', async () => {
|
|
render(<InAppNotificationsPage />);
|
|
|
|
await waitFor(() => {
|
|
// "25 notifications" (total from default handler)
|
|
expect(screen.getByText(/25 notifications/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
});
|