dayBoundary.test.ts4.2 KBView on GitHub import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import {
localDayKey,
msUntilNextLocalMidnight,
} from '@/modules/userTasks/hooks/use-day-boundary';
import { endOfToday } from '@/modules/userTasks/utils/task-filters';
/**
* Every task surface splits "due now" from "upcoming" at `endOfToday()`, which reads the clock at
* call time — inside a `useMemo` whose dependencies are all DATA. Nothing in those arrays moves
* when the day does, so a tab left open holds the boundary it was opened with. Past midnight,
* everything due TODAY reads as `> cutoff` and files under Upcoming, out of its own group column,
* which then collapses into the hidden-columns rail for being empty.
*
* `useDayKey` is the missing dependency. Its two pure halves are tested here; that the surfaces
* actually depend on it is asserted structurally below, because a dependency array is exactly the
* kind of thing that gets dropped in a refactor and cannot fail a behavioural test.
*/
describe('local day boundary', () => {
describe('localDayKey', () => {
it('is the LOCAL calendar day, not the UTC one', () => {
// 2026-09-01T02:30:00Z is still 31 August in every US timezone. Reading the UTC day here
// would disagree with endOfToday(), which is local — and a boundary that two functions
// disagree about is the whole bug class this hook exists for.
const evening = new Date(2026, 7, 31, 20, 30, 0); // 31 Aug, 20:30 local
expect(localDayKey(evening)).toBe('2026-08-31');
});
it('zero-pads, so keys compare as strings in calendar order', () => {
expect(localDayKey(new Date(2026, 0, 5, 12, 0, 0))).toBe('2026-01-05');
});
it('changes exactly once across midnight', () => {
const before = new Date(2026, 8, 1, 23, 59, 59);
const after = new Date(2026, 8, 2, 0, 0, 1);
expect(localDayKey(before)).toBe('2026-09-01');
expect(localDayKey(after)).toBe('2026-09-02');
});
it('names the same day endOfToday() bounds', () => {
const cutoff = new Date(endOfToday());
expect(localDayKey(cutoff)).toBe(localDayKey());
});
});
describe('msUntilNextLocalMidnight', () => {
it('lands just AFTER the boundary, never on it', () => {
// Firing exactly on midnight can round a millisecond early, recompute the same day, and
// re-arm for ~0ms — a tight loop for the rest of the second. The slack is what prevents it.
const now = new Date(2026, 8, 1, 23, 59, 59, 0);
const delay = msUntilNextLocalMidnight(now);
const firesAt = new Date(now.getTime() + delay);
expect(firesAt.getTime()).toBeGreaterThan(new Date(2026, 8, 2, 0, 0, 0, 0).getTime());
expect(localDayKey(firesAt)).toBe('2026-09-02');
});
it('is always positive, including at the first millisecond of a day', () => {
expect(msUntilNextLocalMidnight(new Date(2026, 8, 1, 0, 0, 0, 0))).toBeGreaterThan(0);
expect(msUntilNextLocalMidnight(new Date(2026, 8, 1, 12, 0, 0, 0))).toBeGreaterThan(0);
expect(msUntilNextLocalMidnight(new Date(2026, 8, 1, 23, 59, 59, 999))).toBeGreaterThan(0);
});
it('never schedules more than a day and a half out', () => {
for (let hour = 0; hour < 24; hour++) {
const delay = msUntilNextLocalMidnight(new Date(2026, 8, 1, hour, 30, 0));
expect(delay).toBeLessThan(36 * 60 * 60 * 1000);
}
});
});
/**
* Structural, deliberately. `[..., dayKey]` is a dependency array — dropping it changes no type,
* fails no render test, and silently restores a bug that only appears if you leave a tab open
* overnight, which no test suite does.
*/
describe('the surfaces that bucket by today depend on it', () => {
const read = (relative: string) =>
readFileSync(join(__dirname, '..', '..', '..', 'modules', 'userTasks', relative), 'utf8');
const SURFACES = [
'components/TaskKanbanBoard.tsx',
'components/TaskListView.tsx',
'hooks/use-task-group-buckets.ts',
];
it.each(SURFACES)('%s calls useDayKey and puts it in a dependency array', (relative) => {
const source = read(relative);
expect(source).toContain('useDayKey');
expect(source).toMatch(/dayKey\]/);
});
});
});