import { describe, expect, it } from 'vitest' import { mountWithVuexy } from '../utils/mountWithVuexy' import PerformanceBlock from '@/components/timetable/PerformanceBlock.vue' import { ArtistEngagementStatus, type Performance } from '@/types/timetable' function makePerformance(overrides: Partial = {}): Performance { return { id: 'p1', engagement_id: 'e1', event_id: 'ev1', stage_id: 's1', lane: 0, lane_resolved: 0, start_at: '2026-07-10T18:00:00.000Z', end_at: '2026-07-10T19:00:00.000Z', version: 1, notes: null, warnings: [], engagement: { id: 'e1', organisation_id: 'o1', artist_id: 'a1', event_id: 'ev1', booking_status: { value: ArtistEngagementStatus.CONFIRMED, label: 'Bevestigd' }, project_leader_id: null, fee_amount: '1000.00', fee_currency: 'EUR', fee_type: { value: null, label: null }, buma_applicable: true, buma_percentage: '7.00', buma_handled_by: { value: null, label: null }, vat_applicable: true, vat_percentage: '21.00', deal_breakdown: null, deposit_percentage: null, deposit_due_date: null, balance_due_date: null, payment_status: { value: null, label: null }, crew_count: 0, guests_count: 0, requested_at: null, option_expires_at: null, advance_open_from: null, advance_open_to: null, advancing_completed_count: 3, advancing_total_count: 5, notes: null, computed: { buma_amount: 70, vat_grondslag: 1070, vat_amount: 224.7, breakdown_total: 0, total_cost: 1294.7 }, created_at: null, updated_at: null, deleted_at: null, artist: { id: 'a1', organisation_id: 'o1', name: 'Devin Wild', slug: 'devin-wild', default_genre_id: null, default_draw: null, star_rating: null, home_base_country: null, agent_company_id: null, notes: null, engagements_summary: { lifetime_count: 1, upcoming_count: 1 }, created_at: null, updated_at: null, deleted_at: null, }, }, stage: { id: 's1', event_id: 'ev1', name: 'Hardstyle District', color: '#e85d75', capacity: 1000, sort_order: 0, created_at: null, updated_at: null, }, created_at: null, updated_at: null, deleted_at: null, ...overrides, } } const baseProps = { leftPx: 0, widthPx: 200, topPx: 0, heightPx: 44, } describe('PerformanceBlock — visual states', () => { it.each([ [ArtistEngagementStatus.OPTION, 'tt-perf-block--status-option'], [ArtistEngagementStatus.CONFIRMED, 'tt-perf-block--status-confirmed'], [ArtistEngagementStatus.CANCELLED, 'tt-perf-block--status-cancelled'], ])('renders status %s with the matching CSS-token class', (status, expectedClass) => { const perf = makePerformance({ engagement: { ...makePerformance().engagement!, booking_status: { value: status, label: 'X' }, }, }) const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: perf, ...baseProps } }) const block = wrapper.find('[data-perf-id="p1"]') expect(block.classes()).toContain(expectedClass) expect(block.attributes('data-status')).toBe(status) // CSS variable resolves via the loaded token sheet — proves the class // truly maps to the CSS custom property. const cssVarName = `--tt-status-${status}-bg` const value = getComputedStyle(document.documentElement).getPropertyValue(cssVarName).trim() expect(value.length).toBeGreaterThan(0) }) it('renders the capacity icon when crew + guests > stage.capacity', () => { const perf = makePerformance({ engagement: { ...makePerformance().engagement!, crew_count: 600, guests_count: 600, }, }) const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: perf, ...baseProps } }) expect(wrapper.find('.tt-perf-block__capacity').exists()).toBe(true) }) it('omits the capacity icon when sum ≤ capacity', () => { const perf = makePerformance({ engagement: { ...makePerformance().engagement!, crew_count: 100, guests_count: 100, }, }) const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: perf, ...baseProps } }) expect(wrapper.find('.tt-perf-block__capacity').exists()).toBe(false) }) it('omits capacity icon when stage.capacity is null', () => { const perf = makePerformance({ stage: { ...makePerformance().stage!, capacity: null }, engagement: { ...makePerformance().engagement!, crew_count: 5000, guests_count: 5000, }, }) const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: perf, ...baseProps } }) expect(wrapper.find('.tt-perf-block__capacity').exists()).toBe(false) }) it('renders B2B left dot when b2bLeft prop true', () => { const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: makePerformance(), b2bLeft: true, ...baseProps } }) expect(wrapper.find('.tt-perf-block__b2b--left').exists()).toBe(true) expect(wrapper.find('.tt-perf-block__b2b--right').exists()).toBe(false) }) it('renders B2B right dot when b2bRight prop true', () => { const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: makePerformance(), b2bRight: true, ...baseProps } }) expect(wrapper.find('.tt-perf-block__b2b--right').exists()).toBe(true) expect(wrapper.find('.tt-perf-block__b2b--left').exists()).toBe(false) }) it('renders neither dot when both props false', () => { const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: makePerformance(), ...baseProps } }) expect(wrapper.find('.tt-perf-block__b2b').exists()).toBe(false) }) it('applies conflict ring when warnings include "overlap"', () => { const perf = makePerformance({ warnings: ['overlap'] }) const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: perf, ...baseProps } }) expect(wrapper.find('[data-perf-id="p1"]').classes()).toContain('tt-perf-block--conflict') }) it('applies cascade-pulse class when pulse=true', () => { const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: makePerformance(), pulse: true, ...baseProps } }) expect(wrapper.find('[data-perf-id="p1"]').classes()).toContain('tt-cascade-pulse') }) it('aria-label includes artist, time window, stage, status', () => { const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: makePerformance(), ...baseProps } }) const label = wrapper.find('[data-perf-id="p1"]').attributes('aria-label') ?? '' expect(label).toContain('Devin Wild') expect(label).toContain('Hardstyle District') expect(label).toContain('Bevestigd') expect(label).toMatch(/\d{2}:\d{2}/) // time formatted }) it('exposes tabindex=0 for keyboard focus', () => { const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: makePerformance(), ...baseProps } }) expect(wrapper.find('[data-perf-id="p1"]').attributes('tabindex')).toBe('0') }) }) describe('PerformanceBlock — interactions', () => { it('emits select on click with the performance + DOMRect', async () => { const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: makePerformance(), ...baseProps } }) await wrapper.find('[data-perf-id="p1"]').trigger('click') const events = wrapper.emitted('select') expect(events).toHaveLength(1) expect(events![0][0]).toMatchObject({ id: 'p1' }) }) it('emits pointerdown with (event, performance) on pointerdown', async () => { const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: makePerformance(), ...baseProps } }) await wrapper.find('[data-perf-id="p1"]').trigger('pointerdown') const events = wrapper.emitted('pointerdown') expect(events).toHaveLength(1) expect(events![0][1]).toMatchObject({ id: 'p1' }) }) it('emits delete on Delete keypress', async () => { const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: makePerformance(), ...baseProps } }) await wrapper.find('[data-perf-id="p1"]').trigger('keydown', { key: 'Delete' }) expect(wrapper.emitted('delete')).toHaveLength(1) }) it('emits select on Enter keypress', async () => { const { wrapper } = mountWithVuexy(PerformanceBlock, { props: { performance: makePerformance(), ...baseProps } }) await wrapper.find('[data-perf-id="p1"]').trigger('keydown', { key: 'Enter' }) expect(wrapper.emitted('select')).toHaveLength(1) }) })