Public Access
feat: add What's new feature
Dashboard dialog shows unseen news entries. Users mark them seen with "Got it" or revisit via the /whats-new page. Admins publish, edit, and delete entries on /admin/news. Full i18n support and test coverage included.
This commit is contained in:
1 parent
79b086c525
commit
b811255c16
17 files changed
+690
-1
No files matched your search
@@ -0,0 +1,95 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
import {act, cleanup, fireEvent, render, screen} from '@testing-library/react';
|
||||
import {toast} from 'sonner';
|
||||
import WhatsNewDialog from './WhatsNewDialog.jsx';
|
||||
import {newsText, newsTitle} from '../utils/news.js';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {error: vi.fn()}}));
|
||||
|
||||
const NEW = {id: 2, titleEn: 'New feature', titleDe: 'Neue Funktion', textEn: 'English text', textDe: 'Deutscher Text',
|
||||
publishedAt: '2026-10-01T10:00:00Z', seen: false};
|
||||
const ENGLISH_ONLY = {...NEW, id: 3, titleEn: 'English only', titleDe: null, textEn: 'Only English', textDe: null};
|
||||
const OLD = {...NEW, id: 1, titleEn: 'Old', titleDe: 'Alt', seen: true};
|
||||
|
||||
const newsRoutes = (list, seen = json(204)) => fetchRoutes({'GET /api/news': list, 'PUT /api/news/seen': seen});
|
||||
const putCalls = () => fetch.mock.calls.filter(([, init]) => init?.method === 'PUT');
|
||||
|
||||
const showNews = async (...entries) => {
|
||||
newsRoutes(json(200, {results: entries}));
|
||||
render(<WhatsNewDialog/>);
|
||||
await act(async () => undefined);
|
||||
};
|
||||
|
||||
describe('WhatsNewDialog', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows only the unseen entries, German when set and English otherwise', async () => {
|
||||
await showNews(NEW, ENGLISH_ONLY, OLD);
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeTruthy();
|
||||
expect(screen.getByText('Neue Funktion')).toBeTruthy();
|
||||
expect(screen.getByText('Deutscher Text')).toBeTruthy();
|
||||
expect(screen.getByText('English only')).toBeTruthy();
|
||||
expect(screen.getByText('Only English')).toBeTruthy();
|
||||
expect(screen.queryByText('Alt')).toBeNull();
|
||||
});
|
||||
|
||||
it('marks the entries seen and closes on "Got it"', async () => {
|
||||
await showNews(NEW);
|
||||
|
||||
fireEvent.click(screen.getByText('NEWS.GOT_IT'));
|
||||
await act(async () => undefined);
|
||||
|
||||
expect(putCalls().map(([url]) => url)).toEqual([expect.stringContaining('/api/news/seen')]);
|
||||
expect(screen.queryByRole('dialog')).toBeNull();
|
||||
});
|
||||
|
||||
it('closes with X without marking anything seen', async () => {
|
||||
await showNews(NEW);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', {name: 'Close'}));
|
||||
|
||||
expect(screen.queryByRole('dialog')).toBeNull();
|
||||
expect(putCalls()).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['all entries are seen', json(200, {results: [OLD]})],
|
||||
['there are no entries', json(200, {results: []})],
|
||||
['the load fails', json(500, {})],
|
||||
])('shows nothing when %s', async (_, answer) => {
|
||||
newsRoutes(answer);
|
||||
render(<WhatsNewDialog/>);
|
||||
await act(async () => undefined);
|
||||
|
||||
expect(screen.queryByRole('dialog')).toBeNull();
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows an error when the seen mark is not saved', async () => {
|
||||
newsRoutes(json(200, {results: [NEW]}), json(500, {}));
|
||||
render(<WhatsNewDialog/>);
|
||||
await act(async () => undefined);
|
||||
|
||||
fireEvent.click(screen.getByText('NEWS.GOT_IT'));
|
||||
await act(async () => undefined);
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('NEWS.SAVE_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('newsTitle / newsText', () => {
|
||||
it('pick German only for a German UI and a set German field', () => {
|
||||
expect(newsTitle(NEW, 'de-DE')).toBe('Neue Funktion');
|
||||
expect(newsText(NEW, 'de')).toBe('Deutscher Text');
|
||||
expect(newsTitle(NEW, 'en')).toBe('New feature');
|
||||
expect(newsText(ENGLISH_ONLY, 'de')).toBe('Only English');
|
||||
expect(newsTitle(ENGLISH_ONLY, undefined)).toBe('English only');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user