apps/app/tests/unit/lib/timetable/:
- snap.test.ts (5) — rounding, clamp, edge cases
- time-grid.test.ts (6) — px↔min↔ISO roundtrips, formatTickLabel
- conflict.test.ts (8) — overlap, endpoint-touching, lane/stage scoping, cancelled exclusion
- b2b.test.ts (6) — 0min, 2:59, 3:01, overlap, side-set mapping, threshold constant
- capacity.test.ts (7) — null capacity, missing data, warn/critical, crew+guests preference
- lane.test.ts (8) — Pass 1 + Pass 2, cascade-bump preview, cancelled exclusion
apps/app/tests/unit/composables/:
- useTimetableMutations.test.ts (5) — Idempotency-Key header, optimistic + cascade,
409 VersionMismatch surfaced, park sends null,
createStage POST path
- useDragOrClick.test.ts (3) — onClick fires under threshold, onDragStart+End
above threshold, Esc cancels mid-flight
apps/app/tests/unit/schemas/timetable.test.ts (8) — payload + response zod parsers
apps/app/tests/unit/lib/idempotencyKey.test.ts (3) — 6-30 char range, 24-hex, uniqueness
apps/app/tests/unit/stores/useTimetableStore.test.ts (5) — defaults, toggleStatus, drag state, null guard
Refactor: useTimetableMutations.move now throws Error instances (no-throw-literal)
so AxiosError.message and the VersionMismatchError shape both bubble through .catch().
Test count: 252 → 319 (+67). All 42 files pass.
Out of scope this session (added to BACKLOG):
- ART-PERFORMANCEBLOCK-COMPONENT-TESTS — Vuetify intentionally not loaded in
vitest.config.ts; a Vuexy-stub setup for component-mount tests is one PR of
its own. Pure rendering logic (capacity, B2B, conflict) is fully covered at
the lib/ layer.
- ART-AXE-CORE-A11Y-TESTS — axe-core not yet installed in the repo. The
aria-label structure on PerformanceBlock + aria-live on the page entry are
authored to pass an axe scan when added.
- ART-INTEGRATION-FLOW-TEST — full add → drag → resize → park flow needs
Vuetify + router + msw setup; defer with the component tests above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
2.0 KiB
TypeScript
77 lines
2.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { CAPACITY_TOLERANCE, evaluateCapacity } from '@/lib/timetable/capacity'
|
||
import type { ArtistEngagement, Performance, Stage } from '@/types/timetable'
|
||
|
||
const stage: Stage = {
|
||
id: 's1',
|
||
event_id: 'ev1',
|
||
name: 'Hardstyle',
|
||
color: '#ff0000',
|
||
capacity: 1000,
|
||
sort_order: 0,
|
||
created_at: null,
|
||
updated_at: null,
|
||
}
|
||
|
||
const perf: Performance = {
|
||
id: 'p1',
|
||
engagement_id: 'e1',
|
||
event_id: 'ev1',
|
||
stage_id: 's1',
|
||
lane: 0,
|
||
lane_resolved: 0,
|
||
start_at: null,
|
||
end_at: null,
|
||
version: 0,
|
||
notes: null,
|
||
warnings: [],
|
||
created_at: null,
|
||
updated_at: null,
|
||
deleted_at: null,
|
||
}
|
||
|
||
function eng(crew: number, guests: number, draw: number | null = null): ArtistEngagement {
|
||
return {
|
||
crew_count: crew,
|
||
guests_count: guests,
|
||
artist: draw === null ? undefined : { default_draw: draw } as ArtistEngagement['artist'],
|
||
} as ArtistEngagement
|
||
}
|
||
|
||
describe('evaluateCapacity', () => {
|
||
it('returns null when stage has no capacity', () => {
|
||
expect(evaluateCapacity(perf, { ...stage, capacity: null }, eng(0, 0, 500))).toBeNull()
|
||
})
|
||
|
||
it('returns null when no expected attendance is available', () => {
|
||
expect(evaluateCapacity(perf, stage, eng(0, 0))).toBeNull()
|
||
})
|
||
|
||
it('returns null when below the tolerance', () => {
|
||
expect(evaluateCapacity(perf, stage, eng(0, 0, 1100))).toBeNull()
|
||
})
|
||
|
||
it('returns warn when ratio between tolerance and 1.5×', () => {
|
||
const result = evaluateCapacity(perf, stage, eng(0, 0, 1200))
|
||
|
||
expect(result?.level).toBe('warn')
|
||
})
|
||
|
||
it('returns critical when ratio > 1.5', () => {
|
||
const result = evaluateCapacity(perf, stage, eng(0, 0, 1700))
|
||
|
||
expect(result?.level).toBe('critical')
|
||
})
|
||
|
||
it('prefers crew + guests when present', () => {
|
||
const result = evaluateCapacity(perf, stage, eng(800, 800))
|
||
|
||
expect(result?.expected).toBe(1600)
|
||
expect(result?.level).toBe('critical')
|
||
})
|
||
|
||
it('exposes the tolerance constant', () => {
|
||
expect(CAPACITY_TOLERANCE).toBeGreaterThan(1)
|
||
})
|
||
})
|