From 1e7eba80a8b9bcf0eba560a7664ab723d26e56f1 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sat, 9 May 2026 03:39:48 +0200 Subject: [PATCH] test(timetable): StageRow lane stacking + Wachtrij rendering & drag (Step 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StageRow.test.ts (5): - renders one PerformanceBlock per performance - lane_resolved drives vertical stacking (different lanes → different topPx) - empty stage row renders zero blocks - horizontal position = (start_at - gridStart) × pxPerMin (60 min × 2 = 120px) - block-pointerdown event bubbles up as blockPointerdown Wachtrij.test.ts (5): - one card per parked performance - empty wachtrij shows "Geen optredens" copy - card pointerdown emits cardPointerdown with the parked performance - card click emits cardSelect with the performance + DOMRect - count badge reflects performances.length Test count: 350 → 360. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/app/tests/component/StageRow.test.ts | 131 ++++++++++++++++++++++ apps/app/tests/component/Wachtrij.test.ts | 89 +++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 apps/app/tests/component/StageRow.test.ts create mode 100644 apps/app/tests/component/Wachtrij.test.ts diff --git a/apps/app/tests/component/StageRow.test.ts b/apps/app/tests/component/StageRow.test.ts new file mode 100644 index 00000000..ddaf1ef9 --- /dev/null +++ b/apps/app/tests/component/StageRow.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it } from 'vitest' +import { mountWithVuexy } from '../utils/mountWithVuexy' +import StageRow from '@/components/timetable/StageRow.vue' +import { ArtistEngagementStatus, type Performance, type Stage } from '@/types/timetable' + +const stage: Stage = { + id: 's1', + event_id: 'ev1', + name: 'Hardstyle District', + color: '#e85d75', + capacity: 1000, + sort_order: 0, + created_at: null, + updated_at: null, +} + +function perf(id: string, lane_resolved: number, start: string, end: string): Performance { + return { + id, + engagement_id: 'e1', + event_id: 'ev1', + stage_id: 's1', + lane: lane_resolved, + lane_resolved, + start_at: start, + end_at: end, + version: 1, + notes: null, + warnings: [], + engagement: { + booking_status: { value: ArtistEngagementStatus.CONFIRMED, label: 'Bevestigd' }, + } as Performance['engagement'], + stage, + created_at: null, + updated_at: null, + deleted_at: null, + } +} + +const baseProps = { + stage, + gridStartIso: '2026-07-10T18:00:00.000Z', + totalMinutes: 13 * 60, + pxPerMin: 2, + b2bLeftSet: new Set(), + b2bRightSet: new Set(), + pulseSet: new Set(), + selectedId: null, + dragOriginId: null, +} + +describe('StageRow — lane stacking + rendering', () => { + it('renders one PerformanceBlock per performance', () => { + const { wrapper } = mountWithVuexy(StageRow, { + props: { + ...baseProps, + performances: [ + perf('a', 0, '2026-07-10T18:00:00Z', '2026-07-10T19:00:00Z'), + perf('b', 0, '2026-07-10T19:00:00Z', '2026-07-10T20:00:00Z'), + ], + laneCount: 1, + }, + }) + + expect(wrapper.findAll('[data-perf-id]')).toHaveLength(2) + }) + + it('stacks performances by lane_resolved (different lanes = different topPx)', () => { + const { wrapper } = mountWithVuexy(StageRow, { + props: { + ...baseProps, + performances: [ + perf('a', 0, '2026-07-10T18:00:00Z', '2026-07-10T19:30:00Z'), + perf('b', 1, '2026-07-10T19:00:00Z', '2026-07-10T20:00:00Z'), + ], + laneCount: 2, + }, + }) + + const blockA = wrapper.find('[data-perf-id="a"]') + const blockB = wrapper.find('[data-perf-id="b"]') + + const topA = Number(/inset-block-start: (\d+)px/.exec(blockA.attributes('style') ?? '')?.[1] ?? -1) + const topB = Number(/inset-block-start: (\d+)px/.exec(blockB.attributes('style') ?? '')?.[1] ?? -1) + + expect(topA).toBeGreaterThanOrEqual(0) + expect(topB).toBeGreaterThan(topA) + }) + + it('renders zero blocks for an empty stage row', () => { + const { wrapper } = mountWithVuexy(StageRow, { + props: { + ...baseProps, + performances: [], + laneCount: 1, + }, + }) + + expect(wrapper.findAll('[data-perf-id]')).toHaveLength(0) + }) + + it('positions blocks horizontally based on minutes-since-gridStart × pxPerMin', () => { + const { wrapper } = mountWithVuexy(StageRow, { + props: { + ...baseProps, + + // Anchor 18:00; perf starts 19:00 (60 min later) at 2px/min → leftPx=120 + performances: [perf('a', 0, '2026-07-10T19:00:00Z', '2026-07-10T20:00:00Z')], + laneCount: 1, + }, + }) + + const block = wrapper.find('[data-perf-id="a"]') + const left = Number(/inset-inline-start: (\d+)px/.exec(block.attributes('style') ?? '')?.[1] ?? -1) + + expect(left).toBe(120) + }) + + it('forwards block-pointerdown events from a child PerformanceBlock', async () => { + const { wrapper } = mountWithVuexy(StageRow, { + props: { + ...baseProps, + performances: [perf('a', 0, '2026-07-10T18:00:00Z', '2026-07-10T19:00:00Z')], + laneCount: 1, + }, + }) + + await wrapper.find('[data-perf-id="a"]').trigger('pointerdown') + expect(wrapper.emitted('blockPointerdown')).toHaveLength(1) + }) +}) diff --git a/apps/app/tests/component/Wachtrij.test.ts b/apps/app/tests/component/Wachtrij.test.ts new file mode 100644 index 00000000..aaaf20df --- /dev/null +++ b/apps/app/tests/component/Wachtrij.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest' +import { mountWithVuexy } from '../utils/mountWithVuexy' +import Wachtrij from '@/components/timetable/Wachtrij.vue' +import { ArtistEngagementStatus, type Performance } from '@/types/timetable' + +function parked(id: string, name: string, status = ArtistEngagementStatus.REQUESTED): Performance { + return { + id, + engagement_id: `e_${id}`, + event_id: 'ev1', + stage_id: null, + lane: 0, + lane_resolved: 0, + start_at: null, + end_at: null, + version: 1, + notes: null, + warnings: [], + engagement: { + booking_status: { value: status, label: status }, + artist: { name } as Performance['engagement']['artist'], + } as Performance['engagement'], + stage: null, + created_at: null, + updated_at: null, + deleted_at: null, + } +} + +describe('Wachtrij — parked-card list', () => { + it('renders one card per parked performance', () => { + const { wrapper } = mountWithVuexy(Wachtrij, { + props: { + performances: [parked('p1', 'Devin Wild'), parked('p2', 'Da Tweekaz')], + selectedId: null, + }, + }) + + expect(wrapper.findAll('[data-perf-id]')).toHaveLength(2) + }) + + it('renders the empty-state copy when no performances are parked', () => { + const { wrapper } = mountWithVuexy(Wachtrij, { + props: { performances: [], selectedId: null }, + }) + + expect(wrapper.text()).toMatch(/Geen optredens/i) + }) + + it('forwards a card pointerdown as cardPointerdown', async () => { + const { wrapper } = mountWithVuexy(Wachtrij, { + props: { + performances: [parked('p1', 'Devin Wild')], + selectedId: null, + }, + }) + + await wrapper.find('[data-perf-id="p1"]').trigger('pointerdown') + + expect(wrapper.emitted('cardPointerdown')).toHaveLength(1) + }) + + it('forwards a card click as cardSelect with performance + DOMRect', async () => { + const { wrapper } = mountWithVuexy(Wachtrij, { + props: { + performances: [parked('p1', 'Devin Wild')], + selectedId: null, + }, + }) + + await wrapper.find('[data-perf-id="p1"]').trigger('click') + + const events = wrapper.emitted('cardSelect') + + expect(events).toHaveLength(1) + expect(events![0][0]).toMatchObject({ id: 'p1' }) + }) + + it('shows the wachtrij item count badge', () => { + const { wrapper } = mountWithVuexy(Wachtrij, { + props: { + performances: [parked('a', 'A'), parked('b', 'B'), parked('c', 'C')], + selectedId: null, + }, + }) + + expect(wrapper.find('.tt-wachtrij__count').text()).toBe('3') + }) +})