import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { defineComponent, h, nextTick, ref } from 'vue' import { mount } from '@vue/test-utils' import { useActiveDay } from '@/composables/timetable/useActiveDay' /** * The `?day` source-of-truth contract (Session 4 follow-up Step 5). * * useActiveDay is mounted inside a host component so the watcher actually * runs (composables that use watch() need an active component instance). */ interface HostExposed { activeDayId: () => string | null setActiveDay: (id: string | null) => void setQueryDay: (id: string | null) => void setValidIds: (ids: string[]) => void replaceMock: ReturnType } function mountSync(initialQuery: string | null, initialValidIds: string[]) { const queryDay = ref(initialQuery) const validIds = ref(initialValidIds) const replaceMock = vi.fn((id: string) => { queryDay.value = id }) const host = defineComponent({ setup(_, { expose }) { const { activeDayId, setActiveDay } = useActiveDay({ queryDay, validIds, replace: replaceMock, }) expose({ activeDayId: () => activeDayId.value, setActiveDay, setQueryDay: (v: string | null) => { queryDay.value = v }, setValidIds: (v: string[]) => { validIds.value = v }, replaceMock, }) return () => h('div') }, }) const wrapper = mount(host) return { wrapper, vm: wrapper.vm as unknown as HostExposed, replaceMock } } describe('useActiveDay — ?day source-of-truth', () => { beforeEach(() => { vi.clearAllMocks() }) afterEach(() => { vi.useRealTimers() }) it('valid ?day=X returns X without rewriting the URL', async () => { const { vm, replaceMock } = mountSync('day_2', ['day_1', 'day_2', 'day_3']) await nextTick() expect(vm.activeDayId()).toBe('day_2') expect(replaceMock).not.toHaveBeenCalled() }) it('missing ?day → fallback to first valid id + URL replaced once', async () => { const { vm, replaceMock } = mountSync(null, ['day_1', 'day_2', 'day_3']) await nextTick() await nextTick() expect(vm.activeDayId()).toBe('day_1') expect(replaceMock).toHaveBeenCalledTimes(1) expect(replaceMock).toHaveBeenCalledWith('day_1') }) it('invalid ?day=DOES_NOT_EXIST → fallback + URL corrected', async () => { const { vm, replaceMock } = mountSync('day_bogus', ['day_1', 'day_2']) await nextTick() await nextTick() expect(vm.activeDayId()).toBe('day_1') expect(replaceMock).toHaveBeenCalledWith('day_1') }) it('cross-org ?day (id absent from validIds) → fallback to first valid', async () => { // Backend OrganisationScope keeps the cross-org sub-event out of the // returned list; useActiveDay treats it identically to "doesn't exist". const { vm, replaceMock } = mountSync('day_other_org', ['day_a']) await nextTick() await nextTick() expect(vm.activeDayId()).toBe('day_a') expect(replaceMock).toHaveBeenCalledWith('day_a') }) it('empty validIds → activeDayId is null and URL is not touched', async () => { const { vm, replaceMock } = mountSync('day_1', []) await nextTick() await nextTick() expect(vm.activeDayId()).toBeNull() expect(replaceMock).not.toHaveBeenCalled() }) it('setActiveDay(id) calls replace with the new id', async () => { const { vm, replaceMock } = mountSync('day_1', ['day_1', 'day_2']) await nextTick() vm.setActiveDay('day_2') await nextTick() expect(replaceMock).toHaveBeenLastCalledWith('day_2') expect(vm.activeDayId()).toBe('day_2') }) it('setActiveDay(null) is a no-op', async () => { const { vm, replaceMock } = mountSync('day_1', ['day_1', 'day_2']) await nextTick() replaceMock.mockClear() vm.setActiveDay(null) await nextTick() expect(replaceMock).not.toHaveBeenCalled() }) it('external URL change (browser back) propagates to activeDayId', async () => { const { vm } = mountSync('day_1', ['day_1', 'day_2']) await nextTick() expect(vm.activeDayId()).toBe('day_1') vm.setQueryDay('day_2') await nextTick() expect(vm.activeDayId()).toBe('day_2') }) it('validIds populated AFTER mount triggers fallback if ?day was missing', async () => { const { vm, replaceMock } = mountSync(null, []) await nextTick() expect(vm.activeDayId()).toBeNull() expect(replaceMock).not.toHaveBeenCalled() vm.setValidIds(['day_1', 'day_2']) await nextTick() await nextTick() expect(vm.activeDayId()).toBe('day_1') expect(replaceMock).toHaveBeenCalledWith('day_1') }) })