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:
@@ -44,6 +44,15 @@ run = "npm run dev"
|
||||
description = "Run all Go tests"
|
||||
run = "go test ./..."
|
||||
|
||||
[tasks."test:web"]
|
||||
description = "Run frontend Vitest unit tests"
|
||||
dir = "web"
|
||||
run = "npm test"
|
||||
|
||||
[tasks."test:all"]
|
||||
description = "Run Go tests and frontend Vitest tests"
|
||||
depends = ["test", "test:web"]
|
||||
|
||||
[tasks.install]
|
||||
description = "Install frontend npm dependencies"
|
||||
dir = "web"
|
||||
|
||||
894
web/package-lock.json
generated
894
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
@@ -15,10 +17,12 @@
|
||||
"@sveltejs/adapter-auto": "^7.0.1",
|
||||
"@sveltejs/kit": "^2.57.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"svelte": "^5.55.2",
|
||||
"svelte-check": "^4.4.6",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.7"
|
||||
"vite": "^8.0.7",
|
||||
"vitest": "^4.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -45,5 +45,10 @@ export default defineConfig({
|
||||
'/api': 'http://localhost:8080',
|
||||
'/healthz': 'http://localhost:8080'
|
||||
}
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['src/**/*.test.ts']
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user