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"
|
description = "Run all Go tests"
|
||||||
run = "go test ./..."
|
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]
|
[tasks.install]
|
||||||
description = "Install frontend npm dependencies"
|
description = "Install frontend npm dependencies"
|
||||||
dir = "web"
|
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",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
@@ -15,10 +17,12 @@
|
|||||||
"@sveltejs/adapter-auto": "^7.0.1",
|
"@sveltejs/adapter-auto": "^7.0.1",
|
||||||
"@sveltejs/kit": "^2.57.0",
|
"@sveltejs/kit": "^2.57.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
"svelte": "^5.55.2",
|
"svelte": "^5.55.2",
|
||||||
"svelte-check": "^4.4.6",
|
"svelte-check": "^4.4.6",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"vite": "^8.0.7"
|
"vite": "^8.0.7",
|
||||||
|
"vitest": "^4.1.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@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');
|
const d = new Date(dayKey + 'T12:00:00Z');
|
||||||
return (weekdayBit(d.getUTCDay()) & mask) !== 0;
|
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 { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vitest/config';
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -45,5 +45,10 @@ export default defineConfig({
|
|||||||
'/api': 'http://localhost:8080',
|
'/api': 'http://localhost:8080',
|
||||||
'/healthz': 'http://localhost:8080'
|
'/healthz': 'http://localhost:8080'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
globals: true,
|
||||||
|
include: ['src/**/*.test.ts']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user