import { datetimeMessage, deriveEventState, endAfterStartValidator, isoDatetimeValidator, 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); }); }); describe('isoDatetimeValidator', () => { it('returns { invalidDatetime: true } for an empty value (empty string no longer accepted)', () => { expect(isoDatetimeValidator({ value: '' })).toEqual({ invalidDatetime: true }); expect(isoDatetimeValidator({ value: null })).toEqual({ invalidDatetime: true }); expect(isoDatetimeValidator({ value: undefined })).toEqual({ invalidDatetime: true }); }); it('returns null for a valid ISO-8601 datetime', () => { expect(isoDatetimeValidator({ value: '2026-08-15T10:30:00.000Z' })).toBeNull(); }); it('returns { invalidDatetime: true } for a non-datetime string', () => { expect(isoDatetimeValidator({ value: 'not-a-date' })).toEqual({ invalidDatetime: true }); expect(isoDatetimeValidator({ value: 'also-not-a-date' })).toEqual({ invalidDatetime: true }); }); }); describe('datetimeMessage', () => { it('returns the per-field error when controlErrors carries invalidDatetime', () => { expect(datetimeMessage('Event start', 'not-a-date', { invalidDatetime: true })) .toBe('Event start must be a valid ISO-8601 datetime.'); expect(datetimeMessage('Event end', 'garbage', { invalidDatetime: true })) .toBe('Event end must be a valid ISO-8601 datetime.'); }); it('falls back to detecting an unparseable value when controlErrors are absent', () => { expect(datetimeMessage('Event start', 'not-a-date', null)) .toBe('Event start must be a valid ISO-8601 datetime.'); }); it('returns the per-field error for an empty value (empty is now invalid)', () => { expect(datetimeMessage('Event start', '', null)).toBe('Event start must be a valid ISO-8601 datetime.'); expect(datetimeMessage('Event end', '', { invalidDatetime: true })).toBe('Event end must be a valid ISO-8601 datetime.'); }); it('returns null for a valid ISO-8601 datetime', () => { expect(datetimeMessage('Event start', '2026-08-15T10:30:00.000Z', null)).toBeNull(); }); });