feat(allocation): implement resource allocation feature
- Add AllocationController with CRUD + bulk endpoints - Add AllocationValidationService for capacity/estimate validation - Add AllocationMatrixService for optimized matrix queries - Add AllocationPolicy for authorization - Add AllocationResource for API responses - Add frontend allocationService and matrix UI - Add E2E tests for allocation matrix (20 tests) - Add unit tests for validation service and policies - Fix month format conversion (YYYY-MM to YYYY-MM-01)
This commit is contained in:
105
frontend/tests/unit/expert-mode.test.ts
Normal file
105
frontend/tests/unit/expert-mode.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { fireEvent, render, screen } from '@testing-library/svelte';
|
||||
|
||||
function getStoreValue<T>(store: { subscribe: (run: (value: T) => void) => () => void }): T {
|
||||
let value!: T;
|
||||
const unsubscribe = store.subscribe((current) => {
|
||||
value = current;
|
||||
});
|
||||
unsubscribe();
|
||||
return value;
|
||||
}
|
||||
|
||||
describe('4.1 expertMode store', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
(localStorage.getItem as Mock).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it('4.1.1 expertMode defaults to false when not in localStorage', async () => {
|
||||
const store = await import('../../src/lib/stores/expertMode');
|
||||
|
||||
expect(getStoreValue(store.expertMode)).toBe(false);
|
||||
});
|
||||
|
||||
it('4.1.2 expertMode reads "true" from localStorage', async () => {
|
||||
(localStorage.getItem as Mock).mockReturnValue('true');
|
||||
|
||||
const store = await import('../../src/lib/stores/expertMode');
|
||||
|
||||
expect(getStoreValue(store.expertMode)).toBe(true);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith('headroom.capacity.expertMode');
|
||||
});
|
||||
|
||||
it('4.1.3 expertMode ignores invalid localStorage values', async () => {
|
||||
(localStorage.getItem as Mock).mockReturnValue('invalid');
|
||||
|
||||
const store = await import('../../src/lib/stores/expertMode');
|
||||
|
||||
expect(getStoreValue(store.expertMode)).toBe(false);
|
||||
});
|
||||
|
||||
it('4.1.4 toggleExpertMode writes to localStorage', async () => {
|
||||
const store = await import('../../src/lib/stores/expertMode');
|
||||
|
||||
store.toggleExpertMode();
|
||||
|
||||
expect(getStoreValue(store.expertMode)).toBe(true);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('headroom.capacity.expertMode', 'true');
|
||||
});
|
||||
|
||||
it('4.1.5 setExpertMode updates value and localStorage', async () => {
|
||||
const store = await import('../../src/lib/stores/expertMode');
|
||||
|
||||
store.setExpertMode(true);
|
||||
|
||||
expect(getStoreValue(store.expertMode)).toBe(true);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('headroom.capacity.expertMode', 'true');
|
||||
|
||||
store.setExpertMode(false);
|
||||
|
||||
expect(getStoreValue(store.expertMode)).toBe(false);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('headroom.capacity.expertMode', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('4.2 ExpertModeToggle component', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
(localStorage.getItem as Mock).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it.todo('4.2.1 renders with default unchecked state');
|
||||
it.todo('4.2.2 toggles and updates store on click');
|
||||
it.todo('4.2.3 appears right-aligned in container');
|
||||
});
|
||||
|
||||
describe('6.1-6.2 CapacityExpertGrid component layout', () => {
|
||||
it.todo('6.1 renders a row per active team member');
|
||||
it.todo('6.2 renders a column per day of the month');
|
||||
});
|
||||
|
||||
describe('6.3-6.11 Token normalization', () => {
|
||||
it.todo('6.3 normalizes H to { rawToken: "H", numericValue: 0, valid: true }');
|
||||
it.todo('6.4 normalizes O to { rawToken: "O", numericValue: 0, valid: true }');
|
||||
it.todo('6.5 normalizes .5 to { rawToken: "0.5", numericValue: 0.5, valid: true }');
|
||||
it.todo('6.6 normalizes 0.5 to { rawToken: "0.5", numericValue: 0.5, valid: true }');
|
||||
it.todo('6.7 normalizes 1 to { rawToken: "1", numericValue: 1, valid: true }');
|
||||
it.todo('6.8 normalizes 0 to { rawToken: "0", numericValue: 0, valid: true }');
|
||||
it.todo('6.9 marks invalid token 2 as { rawToken: "2", numericValue: null, valid: false }');
|
||||
it.todo('6.10 auto-render: 0 on weekend column becomes O');
|
||||
it.todo('6.11 auto-render: 0 on holiday column becomes H');
|
||||
});
|
||||
|
||||
describe('6.12-6.14 Grid validation and submit', () => {
|
||||
it.todo('6.12 invalid cell shows red border on blur');
|
||||
it.todo('6.13 Submit button disabled when any invalid cell exists');
|
||||
it.todo('6.14 Submit button disabled when no dirty cells exist');
|
||||
});
|
||||
|
||||
describe('8.1-8.4 KPI bar calculations', () => {
|
||||
it.todo('8.1 capacity = sum of all members numeric cell values (person-days)');
|
||||
it.todo('8.2 revenue = sum(member person-days × hourly_rate × 8)');
|
||||
it.todo('8.3 invalid cells contribute 0 to KPI totals');
|
||||
it.todo('8.4 KPI bar updates when a cell value changes');
|
||||
});
|
||||
Reference in New Issue
Block a user