chore(web): add Vitest; add dayCapabilities helper with full test coverage
- Install vitest + jsdom - Add test/test:watch scripts to package.json - Add test:web and test:all tasks to mise.toml - Add dayCapabilities() to utils.ts — single source of truth for what actions are permitted per day (future/today/past, open/closed) - Add DayCapabilities interface to utils.ts - 11 unit tests: dayCapabilities (5 cases), weekDayKeys (3 cases), isWorkday (3 cases)
This commit is contained in:
84
web/src/lib/utils.test.ts
Normal file
84
web/src/lib/utils.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { dayCapabilities, weekDayKeys, isWorkday } from './utils';
|
||||
|
||||
describe('dayCapabilities', () => {
|
||||
const today = '2026-04-30';
|
||||
|
||||
it('future day: only canMarkKind', () => {
|
||||
const cap = dayCapabilities('2026-05-01', today, false);
|
||||
expect(cap.canStartStop).toBe(false);
|
||||
expect(cap.canAddInterval).toBe(false);
|
||||
expect(cap.canEditEntries).toBe(false);
|
||||
expect(cap.canMarkKind).toBe(true);
|
||||
expect(cap.canCloseDay).toBe(false);
|
||||
expect(cap.canReopenDay).toBe(false);
|
||||
});
|
||||
|
||||
it('today, open: full access except reopen', () => {
|
||||
const cap = dayCapabilities(today, today, false);
|
||||
expect(cap.canStartStop).toBe(true);
|
||||
expect(cap.canAddInterval).toBe(true);
|
||||
expect(cap.canEditEntries).toBe(true);
|
||||
expect(cap.canMarkKind).toBe(true);
|
||||
expect(cap.canCloseDay).toBe(true);
|
||||
expect(cap.canReopenDay).toBe(false);
|
||||
});
|
||||
|
||||
it('today, closed: only markKind and reopen', () => {
|
||||
const cap = dayCapabilities(today, today, true);
|
||||
expect(cap.canStartStop).toBe(false);
|
||||
expect(cap.canAddInterval).toBe(false);
|
||||
expect(cap.canEditEntries).toBe(false);
|
||||
expect(cap.canMarkKind).toBe(true);
|
||||
expect(cap.canCloseDay).toBe(false);
|
||||
expect(cap.canReopenDay).toBe(true);
|
||||
});
|
||||
|
||||
it('past, open: no start/stop; can add/edit/mark/close', () => {
|
||||
const cap = dayCapabilities('2026-04-29', today, false);
|
||||
expect(cap.canStartStop).toBe(false);
|
||||
expect(cap.canAddInterval).toBe(true);
|
||||
expect(cap.canEditEntries).toBe(true);
|
||||
expect(cap.canMarkKind).toBe(true);
|
||||
expect(cap.canCloseDay).toBe(true);
|
||||
expect(cap.canReopenDay).toBe(false);
|
||||
});
|
||||
|
||||
it('past, closed: only markKind and reopen', () => {
|
||||
const cap = dayCapabilities('2026-04-28', today, true);
|
||||
expect(cap.canStartStop).toBe(false);
|
||||
expect(cap.canAddInterval).toBe(false);
|
||||
expect(cap.canEditEntries).toBe(false);
|
||||
expect(cap.canMarkKind).toBe(true);
|
||||
expect(cap.canCloseDay).toBe(false);
|
||||
expect(cap.canReopenDay).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('weekDayKeys', () => {
|
||||
it('2026-W18: Mon Apr 27 – Sun May 3', () => {
|
||||
const keys = weekDayKeys('2026-W18');
|
||||
expect(keys[0]).toBe('2026-04-27');
|
||||
expect(keys[6]).toBe('2026-05-03');
|
||||
expect(keys).toHaveLength(7);
|
||||
});
|
||||
|
||||
it('2024-W03: Mon Jan 15 – Sun Jan 21', () => {
|
||||
const keys = weekDayKeys('2024-W03');
|
||||
expect(keys[0]).toBe('2024-01-15');
|
||||
expect(keys[6]).toBe('2024-01-21');
|
||||
});
|
||||
|
||||
it('2023-W01: Mon Jan 2 – Sun Jan 8', () => {
|
||||
const keys = weekDayKeys('2023-W01');
|
||||
expect(keys[0]).toBe('2023-01-02');
|
||||
expect(keys[6]).toBe('2023-01-08');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isWorkday', () => {
|
||||
const monToFri = 31; // bits 1+2+4+8+16
|
||||
it('Monday is a workday', () => expect(isWorkday('2026-04-27', monToFri)).toBe(true));
|
||||
it('Saturday is not a workday', () => expect(isWorkday('2026-05-02', monToFri)).toBe(false));
|
||||
it('Sunday is not a workday', () => expect(isWorkday('2026-05-03', monToFri)).toBe(false));
|
||||
});
|
||||
@@ -100,3 +100,61 @@ export function isWorkday(dayKey: string, mask: number): boolean {
|
||||
const d = new Date(dayKey + 'T12:00:00Z');
|
||||
return (weekdayBit(d.getUTCDay()) & mask) !== 0;
|
||||
}
|
||||
|
||||
/** Capabilities for a given day in the day detail panel. */
|
||||
export interface DayCapabilities {
|
||||
canStartStop: boolean;
|
||||
canAddInterval: boolean;
|
||||
canEditEntries: boolean;
|
||||
canMarkKind: boolean;
|
||||
canCloseDay: boolean;
|
||||
canReopenDay: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute what actions are permitted for a given day.
|
||||
*
|
||||
* @param dayKey The day in question (YYYY-MM-DD).
|
||||
* @param today Today's day key (YYYY-MM-DD).
|
||||
* @param isClosed Whether the day has a closed_days row.
|
||||
*/
|
||||
export function dayCapabilities(
|
||||
dayKey: string,
|
||||
today: string,
|
||||
isClosed: boolean
|
||||
): DayCapabilities {
|
||||
const isFuture = dayKey > today;
|
||||
const isToday = dayKey === today;
|
||||
|
||||
if (isFuture) {
|
||||
return {
|
||||
canStartStop: false,
|
||||
canAddInterval: false,
|
||||
canEditEntries: false,
|
||||
canMarkKind: true,
|
||||
canCloseDay: false,
|
||||
canReopenDay: false
|
||||
};
|
||||
}
|
||||
|
||||
if (isClosed) {
|
||||
return {
|
||||
canStartStop: false,
|
||||
canAddInterval: false,
|
||||
canEditEntries: false,
|
||||
canMarkKind: true,
|
||||
canCloseDay: false,
|
||||
canReopenDay: true
|
||||
};
|
||||
}
|
||||
|
||||
// Past or today, open
|
||||
return {
|
||||
canStartStop: isToday,
|
||||
canAddInterval: true,
|
||||
canEditEntries: true,
|
||||
canMarkKind: true,
|
||||
canCloseDay: true,
|
||||
canReopenDay: false
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user