feat(events): add Programma tab to EventTabsNav for timetable access
The timetable canvas page at /events/{event}/timetable was added in
RFC-TIMETABLE Session 4 but had no UI entry point. EventTabsNav now
exposes it as the "Programma" tab between Artiesten and Briefings on
flat events, and between Artiesten and Briefings on festivals (in the
re-ordered tab list, post-Artiesten / pre-Briefings).
Changes:
- baseTabs gains the Programma entry at position 6 (after Artiesten).
- The festival re-order computed switches from positional indexing
(baseTabs[5], [6], [7]) to name-based lookup via a findTab helper —
insertions to baseTabs no longer break the festival branch.
- Icon: tabler-calendar-time. Conservative Dutch label "Programma" —
doesn't collide with "Programmaonderdelen" (the festival sub-events
page) since festivals see both tabs side-by-side.
vitest.config.ts: extend the component-project AutoImport to include
'vue-router' so tests of components that auto-import useRoute/useRouter
mount cleanly. (EventTabsNav was the first such test.)
tests/component/EventTabsNav.test.ts (NEW, 4 assertions):
- Programma tab is rendered with the correct label
- it carries the tabler-calendar-time icon
- the route binding resolves to the events-id-timetable name with the
/events/.../timetable URL pattern
- the tab is also visible on a festival (re-ordered tab list path)
Mocks the useEvents composables so the component skips its skeleton/
error branches and renders tabs immediately.
Test count: 385 → 389.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
168
apps/app/tests/component/EventTabsNav.test.ts
Normal file
168
apps/app/tests/component/EventTabsNav.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { computed, ref } from 'vue'
|
||||
import { mountWithVuexy } from '../utils/mountWithVuexy'
|
||||
import EventTabsNav from '@/components/events/EventTabsNav.vue'
|
||||
|
||||
/**
|
||||
* Programma tab smoke (Session 4 follow-up): asserts the timetable
|
||||
* navigation entry is present, labelled, iconed, and routed correctly
|
||||
* — both for flat events and for festivals (which re-order the tabs).
|
||||
*
|
||||
* useEvents queries are mocked so the component skips its skeleton/error
|
||||
* branches and renders the real tabs immediately. useTransitionEventStatus
|
||||
* is mocked too — the header status menu is out of scope here.
|
||||
*/
|
||||
|
||||
interface MinimalEvent {
|
||||
id: string
|
||||
organisation_id: string
|
||||
name: string
|
||||
slug: string
|
||||
status: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
event_type: 'event' | 'festival' | 'series'
|
||||
event_type_label: string
|
||||
is_festival: boolean
|
||||
is_sub_event: boolean
|
||||
parent_event_id: string | null
|
||||
parent: null
|
||||
sub_event_label: string | null
|
||||
children_count: number
|
||||
allowed_transitions: string[]
|
||||
}
|
||||
|
||||
let eventFixtureCurrent: MinimalEvent
|
||||
|
||||
function eventFixture(overrides: Partial<MinimalEvent> = {}): MinimalEvent {
|
||||
return {
|
||||
id: 'ev_1',
|
||||
organisation_id: 'org_1',
|
||||
name: 'Test Event',
|
||||
slug: 'test-event',
|
||||
status: 'draft',
|
||||
start_date: '2026-07-10',
|
||||
end_date: '2026-07-12',
|
||||
event_type: 'event',
|
||||
event_type_label: 'Evenement',
|
||||
is_festival: false,
|
||||
is_sub_event: false,
|
||||
parent_event_id: null,
|
||||
parent: null,
|
||||
sub_event_label: null,
|
||||
children_count: 0,
|
||||
allowed_transitions: [],
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@/composables/api/useEvents', () => ({
|
||||
useEventDetail: () => ({
|
||||
data: computed(() => eventFixtureCurrent),
|
||||
isLoading: ref(false),
|
||||
isError: ref(false),
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
useEventChildren: () => ({
|
||||
data: computed(() => [] as MinimalEvent[]),
|
||||
isLoading: ref(false),
|
||||
isError: ref(false),
|
||||
}),
|
||||
useTransitionEventStatus: () => ({
|
||||
mutateAsync: vi.fn().mockResolvedValue(undefined),
|
||||
isPending: ref(false),
|
||||
}),
|
||||
|
||||
// EditEventDialog is rendered as a child component; it consults
|
||||
// useUpdateEvent + useUploadEventImage on setup. Mock both so they
|
||||
// return safe no-op shapes.
|
||||
useUpdateEvent: () => ({
|
||||
mutate: vi.fn(),
|
||||
mutateAsync: vi.fn().mockResolvedValue(undefined),
|
||||
isPending: ref(false),
|
||||
}),
|
||||
useUploadEventImage: () => ({
|
||||
mutate: vi.fn(),
|
||||
mutateAsync: vi.fn().mockResolvedValue(undefined),
|
||||
isPending: ref(false),
|
||||
}),
|
||||
}))
|
||||
|
||||
const eventsRoutes = [
|
||||
{ path: '/events/:id', name: 'events-id', component: { template: '<div />' } },
|
||||
{ path: '/events/:id/persons', name: 'events-id-persons', component: { template: '<div />' } },
|
||||
{ path: '/events/:id/crowd-lists', name: 'events-id-crowd-lists', component: { template: '<div />' } },
|
||||
{ path: '/events/:id/time-slots', name: 'events-id-time-slots', component: { template: '<div />' } },
|
||||
{ path: '/events/:id/sections', name: 'events-id-sections', component: { template: '<div />' } },
|
||||
{ path: '/events/:id/artists', name: 'events-id-artists', component: { template: '<div />' } },
|
||||
{ path: '/events/:id/briefings', name: 'events-id-briefings', component: { template: '<div />' } },
|
||||
{ path: '/events/:id/timetable', name: 'events-id-timetable', component: { template: '<div />' } },
|
||||
{ path: '/events/:id/settings', name: 'events-id-settings', component: { template: '<div />' } },
|
||||
{ path: '/events/:id/programmaonderdelen', name: 'events-id-programmaonderdelen', component: { template: '<div />' } },
|
||||
{ path: '/events', name: 'events', component: { template: '<div />' } },
|
||||
{ path: '/', name: 'home', component: { template: '<div />' } },
|
||||
]
|
||||
|
||||
async function mountTabs(event: MinimalEvent) {
|
||||
eventFixtureCurrent = event
|
||||
|
||||
const result = mountWithVuexy(EventTabsNav, {
|
||||
routes: eventsRoutes,
|
||||
initialPath: `/events/${event.id}`,
|
||||
initialState: {
|
||||
auth: { currentOrganisation: { id: 'org_1', name: 'Org' } },
|
||||
},
|
||||
})
|
||||
|
||||
await (result.wrapper as unknown as { __routerReady: Promise<void> }).__routerReady
|
||||
await result.wrapper.vm.$nextTick()
|
||||
await result.wrapper.vm.$nextTick()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
describe('EventTabsNav — Programma tab', () => {
|
||||
it('renders a tab labeled "Programma"', async () => {
|
||||
const { wrapper } = await mountTabs(eventFixture())
|
||||
|
||||
const tabs = wrapper.findAll('.v-tab')
|
||||
const labels = tabs.map(t => t.text())
|
||||
|
||||
expect(labels).toContain('Programma')
|
||||
})
|
||||
|
||||
it('uses the tabler-calendar-time icon on the Programma tab', async () => {
|
||||
const { wrapper } = await mountTabs(eventFixture())
|
||||
|
||||
const programmaTab = wrapper.findAll('.v-tab').find(t => t.text() === 'Programma')
|
||||
|
||||
expect(programmaTab).toBeDefined()
|
||||
expect(programmaTab!.html()).toContain('tabler-calendar-time')
|
||||
})
|
||||
|
||||
it('the Programma tab targets the events-id-timetable route', async () => {
|
||||
const { wrapper } = await mountTabs(eventFixture())
|
||||
|
||||
const programmaTab = wrapper.findAll('.v-tab').find(t => t.text() === 'Programma')
|
||||
|
||||
expect(programmaTab).toBeDefined()
|
||||
|
||||
// Two independent proofs that the tab is wired to the right route:
|
||||
// - the rendered VTab `value` attribute equals the route name
|
||||
// - the resolved href (with whatever id is in the test route) ends in /timetable
|
||||
expect(programmaTab!.attributes('value')).toBe('events-id-timetable')
|
||||
expect(programmaTab!.html()).toMatch(/href="\/events\/[^/"]+\/timetable"/)
|
||||
})
|
||||
|
||||
it('also exposes the Programma tab on a festival (re-ordered tabs)', async () => {
|
||||
const { wrapper } = await mountTabs(eventFixture({
|
||||
is_festival: true,
|
||||
event_type: 'festival',
|
||||
sub_event_label: 'dag',
|
||||
}))
|
||||
|
||||
const labels = wrapper.findAll('.v-tab').map(t => t.text())
|
||||
|
||||
expect(labels).toContain('Programma')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user