// @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(); 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('marks the entries seen when closed with X', async () => { await showNews(NEW); fireEvent.click(screen.getByRole('button', {name: 'DIALOG.CLOSE'})); await act(async () => undefined); expect(screen.queryByRole('dialog')).toBeNull(); expect(putCalls()).toHaveLength(1); }); it('marks the entries seen when closed with Esc', async () => { await showNews(NEW); fireEvent.keyDown(screen.getByRole('dialog'), {key: 'Escape'}); await act(async () => undefined); expect(screen.queryByRole('dialog')).toBeNull(); expect(putCalls()).toHaveLength(1); }); 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(); 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(); 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'); }); });