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>
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
import { mount } from '@vue/test-utils'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { defineComponent, h } from 'vue'
|
|
import { useDragOrClick } from '@/composables/timetable/useDragOrClick'
|
|
|
|
interface ComponentInstance {
|
|
begin: (e: Event) => void
|
|
}
|
|
|
|
function makeHost(opts: Parameters<typeof useDragOrClick>[0]) {
|
|
return defineComponent({
|
|
setup(_, { expose }) {
|
|
const ctl = useDragOrClick(opts)
|
|
|
|
expose({ begin: ctl.begin })
|
|
|
|
return () => h('div')
|
|
},
|
|
})
|
|
}
|
|
|
|
function makePointerEvent(type: string, x: number, y: number, pointerId = 1): PointerEvent {
|
|
const e = new Event(type, { bubbles: true, cancelable: true }) as PointerEvent
|
|
|
|
Object.defineProperty(e, 'pointerId', { value: pointerId })
|
|
Object.defineProperty(e, 'clientX', { value: x })
|
|
Object.defineProperty(e, 'clientY', { value: y })
|
|
|
|
return e
|
|
}
|
|
|
|
describe('useDragOrClick', () => {
|
|
it('fires onClick when movement < threshold', async () => {
|
|
const onClick = vi.fn()
|
|
const onDragStart = vi.fn()
|
|
const wrapper = mount(makeHost({ thresholdPx: 4, onClick, onDragStart }))
|
|
const inst = wrapper.vm as unknown as ComponentInstance
|
|
|
|
inst.begin(makePointerEvent('pointerdown', 10, 10))
|
|
window.dispatchEvent(makePointerEvent('pointermove', 11, 11))
|
|
window.dispatchEvent(makePointerEvent('pointerup', 11, 11))
|
|
|
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
expect(onDragStart).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('enters drag mode and emits onDragStart + onDragEnd when movement crosses threshold', async () => {
|
|
const onClick = vi.fn()
|
|
const onDragStart = vi.fn()
|
|
const onDragEnd = vi.fn()
|
|
const wrapper = mount(makeHost({ thresholdPx: 4, onClick, onDragStart, onDragEnd }))
|
|
const inst = wrapper.vm as unknown as ComponentInstance
|
|
|
|
inst.begin(makePointerEvent('pointerdown', 10, 10))
|
|
window.dispatchEvent(makePointerEvent('pointermove', 50, 10))
|
|
window.dispatchEvent(makePointerEvent('pointerup', 50, 10))
|
|
|
|
expect(onDragStart).toHaveBeenCalledTimes(1)
|
|
expect(onDragEnd).toHaveBeenCalledTimes(1)
|
|
expect(onClick).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('Esc cancels an in-flight drag', async () => {
|
|
const onDragEnd = vi.fn()
|
|
const wrapper = mount(makeHost({ thresholdPx: 4, onDragEnd }))
|
|
const inst = wrapper.vm as unknown as ComponentInstance
|
|
|
|
inst.begin(makePointerEvent('pointerdown', 10, 10))
|
|
window.dispatchEvent(makePointerEvent('pointermove', 50, 10))
|
|
|
|
const esc = new KeyboardEvent('keydown', { key: 'Escape' })
|
|
|
|
window.dispatchEvent(esc)
|
|
|
|
expect(onDragEnd).toHaveBeenCalledWith(expect.anything(), true)
|
|
})
|
|
})
|