Public Access
feat(dashboard): show admin hints and add the hints admin page
This commit is contained in:
1 parent
6601277961
commit
01ae372b2d
12 files changed
+700
-6
No files matched your search
@@ -0,0 +1,123 @@
|
||||
// @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';
|
||||
|
||||
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 answer = (status, body) => ({ok: status < 300, status, headers: new Headers(), json: async () => body});
|
||||
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');
|
||||
globalThis.fetch = vi.fn(async (url, init) => {
|
||||
if (init?.method === 'POST') return answer(201, {id: 3});
|
||||
if (init?.method === 'PUT') return answer(204);
|
||||
return answer(200, {results: [ACTIVE, ENDED]});
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
fetch.mockImplementation(async (url, init) =>
|
||||
(init?.method === 'POST' ? answer(400, {code: 'INVALID_HINT'}) : answer(200, {results: []})));
|
||||
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'};
|
||||
fetch.mockImplementation(async () => answer(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 () => {
|
||||
fetch.mockImplementation(async () => answer(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');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user