mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 08:16:22 +00:00
134 lines
6.8 KiB
TypeScript
134 lines
6.8 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { mostVisibleMonthDay,
|
|
baseRange, computeScrollWindow, fixedScrollWindowState, freshScrollWindowState, growScrollWindow,
|
|
normalizeScrollWindowState, scrollWindowContains, SCROLL_WINDOW_MAX, SCROLL_WINDOW_STEP,
|
|
type ScrollWindowOptions,
|
|
} from '../calendar-scroll-window';
|
|
|
|
// #759: every calendar view keeps one window of days around the focused
|
|
// day; edges double, navigation inside the window does not reset it.
|
|
|
|
const opts: ScrollWindowOptions = { weekStartsOn: 1 };
|
|
const key = (d: Date) => d.toISOString().slice(0, 10);
|
|
// Local-midnight dates render in the test's timezone; compare by local fields.
|
|
const local = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
|
|
describe('baseRange', () => {
|
|
it('is the day itself, the week, the month grid, or 30 days for the agenda', () => {
|
|
const wed = new Date(2026, 8, 9); // Wednesday
|
|
expect(local(baseRange('day', wed, opts).start)).toBe('2026-09-09');
|
|
expect(local(baseRange('day', wed, opts).end)).toBe('2026-09-09');
|
|
expect(local(baseRange('week', wed, opts).start)).toBe('2026-09-07');
|
|
expect(local(baseRange('week', wed, opts).end)).toBe('2026-09-13');
|
|
expect(local(baseRange('month', wed, opts).start)).toBe('2026-08-31');
|
|
expect(local(baseRange('month', wed, opts).end)).toBe('2026-10-04');
|
|
expect(local(baseRange('agenda', wed, opts).start)).toBe('2026-09-09');
|
|
expect(local(baseRange('agenda', wed, opts).end)).toBe('2026-10-09');
|
|
});
|
|
|
|
it('uses the locale month grid when one is supplied', () => {
|
|
const grid = [new Date(2026, 7, 24), new Date(2026, 7, 25), new Date(2026, 9, 11)];
|
|
const range = baseRange('month', new Date(2026, 8, 9), { ...opts, monthGridDays: () => grid });
|
|
expect(local(range.start)).toBe('2026-08-24');
|
|
expect(local(range.end)).toBe('2026-10-11');
|
|
});
|
|
});
|
|
|
|
describe('computeScrollWindow', () => {
|
|
it('starts as the base range plus the initial days after it, snapped to whole weeks', () => {
|
|
const win = computeScrollWindow(freshScrollWindowState('month', new Date(2026, 8, 9)), opts);
|
|
expect(local(win.start)).toBe('2026-08-31');
|
|
// 2026-10-04 + 30 days = 2026-11-03 (Tuesday) -> end of that week
|
|
expect(local(win.end)).toBe('2026-11-08');
|
|
expect(win.canExtendStart).toBe(true);
|
|
expect(win.canExtendEnd).toBe(true);
|
|
});
|
|
|
|
it('is exactly one period when free scrolling is off (agenda keeps its 30 days)', () => {
|
|
const month = computeScrollWindow(fixedScrollWindowState('month', new Date(2026, 8, 9)), opts);
|
|
expect(local(month.start)).toBe('2026-08-31');
|
|
expect(local(month.end)).toBe('2026-10-04');
|
|
const week = computeScrollWindow(fixedScrollWindowState('week', new Date(2026, 8, 9)), opts);
|
|
expect([local(week.start), local(week.end)]).toEqual(['2026-09-07', '2026-09-13']);
|
|
const day = computeScrollWindow(fixedScrollWindowState('day', new Date(2026, 8, 9)), opts);
|
|
expect([local(day.start), local(day.end)]).toEqual(['2026-09-09', '2026-09-09']);
|
|
const agenda = computeScrollWindow(fixedScrollWindowState('agenda', new Date(2026, 8, 9)), opts);
|
|
expect([local(agenda.start), local(agenda.end)]).toEqual(['2026-09-09', '2026-10-09']);
|
|
});
|
|
|
|
it('does not snap the day view to weeks', () => {
|
|
const win = computeScrollWindow(freshScrollWindowState('day', new Date(2026, 8, 9)), opts);
|
|
expect(local(win.start)).toBe('2026-09-09');
|
|
expect(local(win.end)).toBe('2026-10-09');
|
|
});
|
|
|
|
it('reports the limits once a side reached its maximum', () => {
|
|
const state = { ...freshScrollWindowState('week', new Date(2026, 8, 9)), before: SCROLL_WINDOW_MAX.week };
|
|
expect(computeScrollWindow(state, opts).canExtendStart).toBe(false);
|
|
expect(computeScrollWindow(state, opts).canExtendEnd).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('growScrollWindow', () => {
|
|
it('doubles a side from the first step up to the cap and then stays put', () => {
|
|
let state = freshScrollWindowState('agenda', new Date(2026, 8, 9));
|
|
const seen: number[] = [];
|
|
for (let i = 0; i < 8; i++) {
|
|
state = growScrollWindow(state, 'before');
|
|
seen.push(state.before);
|
|
}
|
|
expect(seen).toEqual([SCROLL_WINDOW_STEP, 60, 120, 240, 365, 365, 365, 365]);
|
|
const capped = growScrollWindow(state, 'before');
|
|
expect(capped).toBe(state);
|
|
});
|
|
});
|
|
|
|
describe('normalizeScrollWindowState / scrollWindowContains', () => {
|
|
it('starts over when the view mode changed', () => {
|
|
const month = freshScrollWindowState('month', new Date(2026, 8, 9));
|
|
expect(normalizeScrollWindowState(month, 'month', new Date(2026, 0, 1))).toBe(month);
|
|
const week = normalizeScrollWindowState(month, 'week', new Date(2026, 0, 1));
|
|
expect(week.mode).toBe('week');
|
|
expect(week.anchorKey).toBe('2026-01-01');
|
|
});
|
|
|
|
it('knows whether a navigation target is already loaded', () => {
|
|
const win = computeScrollWindow(freshScrollWindowState('month', new Date(2026, 8, 9)), opts);
|
|
expect(scrollWindowContains(win, 'month', new Date(2026, 9, 15), opts)).toBe(true); // October grid ends Nov 1
|
|
expect(scrollWindowContains(win, 'month', new Date(2026, 10, 15), opts)).toBe(false); // November grid ends Dec 6
|
|
expect(scrollWindowContains(win, 'week', new Date(2026, 7, 31), opts)).toBe(true);
|
|
expect(scrollWindowContains(win, 'week', new Date(2026, 7, 30), opts)).toBe(false);
|
|
});
|
|
|
|
it('keys the anchor by calendar day', () => {
|
|
expect(freshScrollWindowState('day', new Date(2026, 8, 9, 23, 59)).anchorKey).toBe('2026-09-09');
|
|
expect(key(new Date(Date.UTC(2026, 8, 9)))).toBe('2026-09-09');
|
|
});
|
|
});
|
|
|
|
describe('mostVisibleMonthDay', () => {
|
|
// Weeks of 2026, 100px each, from Monday 26 October.
|
|
const rows = [new Date(2026, 9, 26), new Date(2026, 10, 2), new Date(2026, 10, 9), new Date(2026, 10, 16)]
|
|
.map((weekStart, i) => ({ weekStart, top: i * 100, bottom: (i + 1) * 100 }));
|
|
const monthKey = (d: Date) => `${d.getFullYear()}-${d.getMonth()}`;
|
|
|
|
it('names the month of the later week in a pane two weeks high', () => {
|
|
// Six October days and eight November days: not the month of the top week.
|
|
expect(mostVisibleMonthDay(rows, 0, 200, monthKey)).toEqual(new Date(2026, 10, 1));
|
|
});
|
|
|
|
it('names the month before while only the week holding the 1st shows', () => {
|
|
expect(mostVisibleMonthDay(rows, 0, 100, monthKey)).toEqual(new Date(2026, 9, 26));
|
|
});
|
|
|
|
it('counts a week cut by the edge for the part that shows', () => {
|
|
// A sliver of the November week against the whole week of October days.
|
|
expect(mostVisibleMonthDay(rows, 0, 110, monthKey)).toEqual(new Date(2026, 9, 26));
|
|
});
|
|
|
|
it('leaves out rows outside the pane and returns nothing without rows in view', () => {
|
|
expect(mostVisibleMonthDay(rows, 150, 400, monthKey)).toEqual(new Date(2026, 10, 2));
|
|
expect(mostVisibleMonthDay(rows, 900, 1000, monthKey)).toBeNull();
|
|
});
|
|
});
|