// @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');
});
});