Public Access
125 lines
5.7 KiB
React
125 lines
5.7 KiB
React
// @vitest-environment jsdom
|
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
|
import {MemoryRouter} from 'react-router-dom';
|
|
import {toast} from 'sonner';
|
|
import AdminHintsPage from './AdminHintsPage.jsx';
|
|
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
|
|
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
|
|
|
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
|
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
const at = (offsetDays) => new Date(Date.now() + offsetDays * DAY_MS).toISOString();
|
|
const ACTIVE = {id: 2, textEn: 'Server update tonight', textDe: null, kind: 'DISMISS', startsAt: at(-1), endsAt: at(1), status: 'ACTIVE',
|
|
createdBy: 1, createdAt: at(-1), doneCount: 3};
|
|
const ENDED = {...ACTIVE, id: 1, textEn: 'Old news', startsAt: at(-3), endsAt: at(-2), status: 'ENDED', doneCount: 0};
|
|
|
|
const withRoutes = (routes) => fetchRoutes({
|
|
'GET /api/admin/hints': json(200, {results: [ACTIVE, ENDED]}),
|
|
'POST /api/admin/hints': json(201, {id: 3}),
|
|
'PUT /api/admin/hints/2/end': json(204),
|
|
...routes,
|
|
});
|
|
const calls = (method) => fetch.mock.calls.filter(([, init]) => (init?.method ?? 'GET') === method);
|
|
|
|
const renderPage = () => render(<MemoryRouter><PreferencesProvider><AdminHintsPage/></PreferencesProvider></MemoryRouter>);
|
|
|
|
describe('AdminHintsPage', () => {
|
|
beforeEach(() => {
|
|
localStorage.setItem('tbp_session', '1');
|
|
withRoutes({});
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
localStorage.clear();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('lists the hints and offers "End" only on a running one', async () => {
|
|
renderPage();
|
|
|
|
expect(await screen.findByText('Server update tonight')).toBeTruthy();
|
|
expect(screen.getByText('ADMIN_HINTS.STATUS_ACTIVE')).toBeTruthy();
|
|
expect(screen.getByText('ADMIN_HINTS.STATUS_ENDED')).toBeTruthy();
|
|
expect(screen.getAllByText('ADMIN_HINTS.END')).toHaveLength(1);
|
|
|
|
fireEvent.click(screen.getByText('ADMIN_HINTS.END'));
|
|
|
|
await waitFor(() => expect(toast.success).toHaveBeenCalledWith('ADMIN_HINTS.ENDED'));
|
|
expect(calls('PUT')[0][0]).toContain('/api/admin/hints/2/end');
|
|
});
|
|
|
|
it('publishes a trimmed hint and shows a preview', async () => {
|
|
renderPage();
|
|
await screen.findByText('Server update tonight');
|
|
|
|
fireEvent.change(screen.getByLabelText('ADMIN_HINTS.TEXT_EN'), {target: {value: ' Link your Steam account '}});
|
|
fireEvent.change(screen.getByLabelText('ADMIN_HINTS.TEXT_DE'), {target: {value: ' '}});
|
|
fireEvent.click(screen.getByLabelText('ADMIN_HINTS.KIND_ACCEPT'));
|
|
expect(screen.getByText('HINT.OK')).toBeTruthy();
|
|
fireEvent.click(screen.getByText('ADMIN_HINTS.CREATE'));
|
|
|
|
await waitFor(() => expect(toast.success).toHaveBeenCalledWith('ADMIN_HINTS.CREATED'));
|
|
const body = JSON.parse(calls('POST')[0][1].body);
|
|
expect(body).toMatchObject({textEn: 'Link your Steam account', textDe: '', kind: 'ACCEPT'});
|
|
expect(new Date(body.endsAt) - new Date(body.startsAt)).toBe(7 * DAY_MS);
|
|
expect(screen.getByLabelText('ADMIN_HINTS.TEXT_EN').value).toBe('');
|
|
});
|
|
|
|
it('explains an invalid time range', async () => {
|
|
withRoutes({'GET /api/admin/hints': json(200, {results: []}), 'POST /api/admin/hints': json(400, {code: 'INVALID_HINT'})});
|
|
renderPage();
|
|
await screen.findByText('ADMIN_HINTS.EMPTY');
|
|
|
|
fireEvent.change(screen.getByLabelText('ADMIN_HINTS.TEXT_EN'), {target: {value: 'News'}});
|
|
fireEvent.click(screen.getByText('ADMIN_HINTS.CREATE'));
|
|
|
|
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('ADMIN_HINTS.INVALID'));
|
|
});
|
|
|
|
it('takes the status from the server, not from the browser clock', async () => {
|
|
// The server ended the hint by its own clock; by the browser clock the end is still 5 s away.
|
|
const endedOnServer = {...ACTIVE, endsAt: new Date(Date.now() + 5_000).toISOString(), status: 'ENDED'};
|
|
withRoutes({'GET /api/admin/hints': json(200, {results: [endedOnServer]})});
|
|
renderPage();
|
|
|
|
await screen.findByText('Server update tonight');
|
|
|
|
expect(screen.queryByText('ADMIN_HINTS.STATUS_ACTIVE')).toBeNull();
|
|
expect(screen.queryByText('ADMIN_HINTS.END')).toBeNull();
|
|
});
|
|
|
|
it('previews the English text when the German one is only spaces', async () => {
|
|
renderPage();
|
|
await screen.findByText('Server update tonight');
|
|
|
|
fireEvent.change(screen.getByLabelText('ADMIN_HINTS.TEXT_EN'), {target: {value: 'Link your Steam account'}});
|
|
fireEvent.change(screen.getByLabelText('ADMIN_HINTS.TEXT_DE'), {target: {value: ' '}});
|
|
|
|
// The mocked UI language is German.
|
|
expect(document.querySelector('[role="status"] p').textContent).toBe('Link your Steam account');
|
|
});
|
|
|
|
it('shows "no access" to a non-admin', async () => {
|
|
withRoutes({'GET /api/admin/hints': json(403, {})});
|
|
renderPage();
|
|
|
|
expect(await screen.findByText('ADMIN_HINTS.NO_ACCESS')).toBeTruthy();
|
|
expect(screen.queryByText('ADMIN_HINTS.CREATE')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('ADMIN_HINTS.DONE_COUNT', () => {
|
|
it('uses the singular for one user', async () => {
|
|
const {default: i18next} = await import('i18next');
|
|
const en = (await import('../../public/i18n/en.json')).default;
|
|
const i18n = i18next.createInstance();
|
|
await i18n.init({lng: 'en', resources: {en: {translation: en}}});
|
|
|
|
expect(i18n.t('ADMIN_HINTS.DONE_COUNT', {count: 1})).not.toContain('users');
|
|
});
|
|
});
|