Files
portal/src/lib/schedule-utils.spec.ts
T

145 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import {
findOverlap,
formatTime,
snapMinutes,
toHm,
validateBlock,
type BlockInput,
type BlockSibling
} from './schedule-utils';
const input = (overrides: Partial<BlockInput> = {}): BlockInput => ({
startMinutes: 6 * 60,
endMinutes: 8 * 60,
proclamationIds: ['p1'],
...overrides
});
const sibling = (startMinutes: number, endMinutes: number, id = 'b1'): BlockSibling => ({
id,
startMinutes,
endMinutes
});
describe('formatTime', () => {
it('formats 12-hour clock labels', () => {
expect(formatTime(0)).toBe('12:00 AM');
expect(formatTime(6 * 60)).toBe('6:00 AM');
expect(formatTime(8 * 60 + 5)).toBe('8:05 AM');
expect(formatTime(12 * 60)).toBe('12:00 PM');
expect(formatTime(13 * 60 + 30)).toBe('1:30 PM');
expect(formatTime(23 * 60 + 59)).toBe('11:59 PM');
});
});
describe('toHm', () => {
it('formats 24-hour HH:MM strings', () => {
expect(toHm(0)).toBe('00:00');
expect(toHm(6 * 60)).toBe('06:00');
expect(toHm(23 * 60 + 59)).toBe('23:59');
});
});
describe('snapMinutes', () => {
it('rounds to the nearest 15 minutes and clamps to the day', () => {
expect(snapMinutes(0)).toBe(0);
expect(snapMinutes(7)).toBe(0);
expect(snapMinutes(8)).toBe(15);
expect(snapMinutes(370)).toBe(375);
expect(snapMinutes(-10)).toBe(0);
expect(snapMinutes(2000)).toBe(1440);
});
});
describe('findOverlap', () => {
it('returns null for adjacent ranges', () => {
expect(
findOverlap({ startMinutes: 8 * 60, endMinutes: 9 * 60 }, [sibling(6 * 60, 8 * 60)])
).toBeNull();
});
it('returns the sibling for a partial overlap', () => {
const other = sibling(7 * 60, 9 * 60);
expect(findOverlap({ startMinutes: 8 * 60, endMinutes: 10 * 60 }, [other])).toBe(other);
});
});
describe('validateBlock', () => {
it('accepts a valid block with no siblings', () => {
expect(validateBlock(input(), [], { isCreate: true })).toEqual({ ok: true });
});
it('rejects equal start and end times', () => {
const result = validateBlock(input({ endMinutes: 6 * 60 }), []);
expect(result).toEqual({
ok: false,
error: "Start time must be before end time. Overnight blocks aren't supported."
});
});
it('rejects an end time past midnight (overnight blocks disallowed)', () => {
const result = validateBlock(input({ startMinutes: 23 * 60, endMinutes: 25 * 60 }), []);
expect(result.ok).toBe(false);
});
it('rejects a negative start time', () => {
expect(validateBlock(input({ startMinutes: -1 }), []).ok).toBe(false);
});
it('requires at least one proclamation', () => {
expect(validateBlock(input({ proclamationIds: [] }), [])).toEqual({
ok: false,
error: 'Select at least one proclamation.'
});
});
it('enforces the four-block maximum on create', () => {
const siblings = [
sibling(0, 60, 'a'),
sibling(60, 120, 'b'),
sibling(120, 180, 'c'),
sibling(180, 240, 'd')
];
const result = validateBlock(input({ startMinutes: 300, endMinutes: 360 }), siblings, {
isCreate: true
});
expect(result).toEqual({ ok: false, error: 'You can have at most 4 blocks.' });
});
it('allows adjacent blocks (end exactly equals next start)', () => {
expect(
validateBlock(input({ startMinutes: 8 * 60, endMinutes: 9 * 60 }), [sibling(6 * 60, 8 * 60)])
).toEqual({
ok: true
});
});
it('rejects an exact duplicate range and names the conflicting block', () => {
const result = validateBlock(input(), [sibling(6 * 60, 8 * 60)]);
expect(result).toEqual({
ok: false,
error: 'This overlaps with your 6:00 AM–8:00 AM block.'
});
});
it('rejects a partial overlap', () => {
const result = validateBlock(input({ startMinutes: 7 * 60, endMinutes: 9 * 60 }), [
sibling(8 * 60, 10 * 60)
]);
expect(result.ok).toBe(false);
});
it('rejects a block that contains another block', () => {
const result = validateBlock(input({ startMinutes: 5 * 60, endMinutes: 11 * 60 }), [
sibling(6 * 60, 8 * 60)
]);
expect(result.ok).toBe(false);
});
it('does not apply the maximum when updating (isCreate false)', () => {
const siblings = [sibling(0, 60, 'a'), sibling(60, 120, 'b'), sibling(120, 180, 'c')];
expect(validateBlock(input({ startMinutes: 300, endMinutes: 360 }), siblings).ok).toBe(true);
});
});