164 lines
5.5 KiB
TypeScript
164 lines
5.5 KiB
TypeScript
import {
|
|
deriveEventState,
|
|
endAfterStartValidator,
|
|
normalizePageTitle,
|
|
pageTitleError,
|
|
pageTitleMessage,
|
|
toDatetimeLocal,
|
|
toIsoUtc,
|
|
} from '../../frontend/src/app/features/admin/general.pure';
|
|
|
|
describe('deriveEventState', () => {
|
|
const start = '2026-01-01T00:00:00Z';
|
|
const end = '2026-01-02T00:00:00Z';
|
|
|
|
it('returns "unconfigured" when start or end is missing', () => {
|
|
expect(deriveEventState('', '')).toBe('unconfigured');
|
|
expect(deriveEventState(start, '')).toBe('unconfigured');
|
|
expect(deriveEventState('', end)).toBe('unconfigured');
|
|
});
|
|
|
|
it('returns "unconfigured" when dates cannot be parsed', () => {
|
|
expect(deriveEventState('not-a-date', end)).toBe('unconfigured');
|
|
});
|
|
|
|
it('returns "countdown" when now is before start', () => {
|
|
const s = new Date(Date.now() + 60_000).toISOString();
|
|
const e = new Date(Date.now() + 120_000).toISOString();
|
|
expect(deriveEventState(s, e)).toBe('countdown');
|
|
});
|
|
|
|
it('returns "running" when now is between start and end', () => {
|
|
const s = new Date(Date.now() - 60_000).toISOString();
|
|
const e = new Date(Date.now() + 60_000).toISOString();
|
|
expect(deriveEventState(s, e)).toBe('running');
|
|
});
|
|
|
|
it('returns "stopped" when now is after end', () => {
|
|
const s = new Date(Date.now() - 120_000).toISOString();
|
|
const e = new Date(Date.now() - 60_000).toISOString();
|
|
expect(deriveEventState(s, e)).toBe('stopped');
|
|
});
|
|
});
|
|
|
|
describe('endAfterStartValidator', () => {
|
|
const ctrl = (start: string, end: string) => ({
|
|
get: (k: string) =>
|
|
k === 'eventStartUtc'
|
|
? { value: start }
|
|
: k === 'eventEndUtc'
|
|
? { value: end }
|
|
: { value: '' },
|
|
});
|
|
|
|
it('returns null when either field is empty', () => {
|
|
expect(endAfterStartValidator(ctrl('', '2026-01-01T00:00:00Z'))).toBeNull();
|
|
expect(endAfterStartValidator(ctrl('2026-01-01T00:00:00Z', ''))).toBeNull();
|
|
});
|
|
|
|
it('returns null when end is strictly after start', () => {
|
|
expect(endAfterStartValidator(ctrl('2026-01-01T00:00:00Z', '2026-01-02T00:00:00Z'))).toBeNull();
|
|
});
|
|
|
|
it('returns { endBeforeStart: true } when end equals start', () => {
|
|
expect(endAfterStartValidator(ctrl('2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'))).toEqual({ endBeforeStart: true });
|
|
});
|
|
|
|
it('returns { endBeforeStart: true } when end is before start', () => {
|
|
expect(endAfterStartValidator(ctrl('2026-01-02T00:00:00Z', '2026-01-01T00:00:00Z'))).toEqual({ endBeforeStart: true });
|
|
});
|
|
});
|
|
|
|
describe('datetime helpers', () => {
|
|
it('toDatetimeLocal formats an ISO timestamp with UTC components', () => {
|
|
expect(toDatetimeLocal('2026-01-02T03:04:00Z')).toBe('2026-01-02T03:04');
|
|
});
|
|
|
|
it('toDatetimeLocal returns empty string for invalid input', () => {
|
|
expect(toDatetimeLocal('')).toBe('');
|
|
expect(toDatetimeLocal('garbage')).toBe('');
|
|
});
|
|
|
|
it('toIsoUtc converts a local datetime back to UTC ISO', () => {
|
|
const iso = toIsoUtc('2026-01-02T03:04');
|
|
expect(iso).toMatch(/^2026-01-02T03:04:00\.000Z$/);
|
|
});
|
|
|
|
it('toIsoUtc returns input verbatim when empty', () => {
|
|
expect(toIsoUtc('')).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('pageTitleError', () => {
|
|
it('returns "required" for empty input', () => {
|
|
expect(pageTitleError('')).toBe('required');
|
|
expect(pageTitleError(null)).toBe('required');
|
|
expect(pageTitleError(undefined)).toBe('required');
|
|
});
|
|
|
|
it('returns "required" for whitespace-only input', () => {
|
|
expect(pageTitleError(' ')).toBe('required');
|
|
expect(pageTitleError('\t\n ')).toBe('required');
|
|
});
|
|
|
|
it('returns null for a valid title', () => {
|
|
expect(pageTitleError('OpenVelo')).toBeNull();
|
|
});
|
|
|
|
it('returns "maxlength" when the raw string exceeds 120 characters', () => {
|
|
expect(pageTitleError('a'.repeat(121))).toBe('maxlength');
|
|
});
|
|
|
|
it('returns null when surrounding whitespace keeps the trimmed length within bounds', () => {
|
|
expect(pageTitleError(' OpenVelo ')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('normalizePageTitle', () => {
|
|
it('returns an empty string for empty input', () => {
|
|
expect(normalizePageTitle('')).toBe('');
|
|
});
|
|
|
|
it('returns an empty string for whitespace-only input', () => {
|
|
expect(normalizePageTitle(' ')).toBe('');
|
|
});
|
|
|
|
it('trims surrounding whitespace from a valid title', () => {
|
|
expect(normalizePageTitle(' OpenVelo ')).toBe('OpenVelo');
|
|
});
|
|
});
|
|
|
|
describe('pageTitleMessage', () => {
|
|
const requiredMsg = 'Page title is required and cannot contain only whitespace.';
|
|
const maxLengthMsg = 'Page title must be 120 characters or fewer.';
|
|
|
|
it('returns the required message for empty input', () => {
|
|
expect(pageTitleMessage('', null)).toBe(requiredMsg);
|
|
});
|
|
|
|
it('returns the required message for whitespace-only input', () => {
|
|
expect(pageTitleMessage(' ', null)).toBe(requiredMsg);
|
|
expect(pageTitleMessage('\t\n ', null)).toBe(requiredMsg);
|
|
});
|
|
|
|
it('returns null for a valid title', () => {
|
|
expect(pageTitleMessage('OpenVelo', null)).toBeNull();
|
|
});
|
|
|
|
it('returns the maxlength message when the raw string exceeds 120 characters', () => {
|
|
expect(pageTitleMessage('a'.repeat(121), null)).toBe(maxLengthMsg);
|
|
});
|
|
|
|
it('surfaces the whitespace validator error when control errors are provided', () => {
|
|
expect(pageTitleMessage(' ', { whitespace: true })).toBe(requiredMsg);
|
|
});
|
|
|
|
it('surfaces the required control error', () => {
|
|
expect(pageTitleMessage('', { required: true })).toBe(requiredMsg);
|
|
});
|
|
|
|
it('surfaces the maxlength control error', () => {
|
|
expect(pageTitleMessage('a'.repeat(121), { maxlength: { requiredLength: 120 } })).toBe(maxLengthMsg);
|
|
});
|
|
});
|