import {describe, expect, it, vi} from 'vitest' import {defineComponent, nextTick, ref} from 'vue' import {mount} from '@vue/test-utils' import { CENTER_OFFSET, VISIBLE_ROWS, loopCorrection, scrollTopForValueIndex, useInfiniteWheel, valueIndexFromScroll, } from './useInfiniteWheel' const H = 40 // itemHeight const LEN = 24 // ex. heures describe('useInfiniteWheel — math pure', () => { it('expose 5 lignes visibles et un offset central de 2', () => { expect(VISIBLE_ROWS).toBe(5) expect(CENTER_OFFSET).toBe(2) }) it('scrollTopForValueIndex et valueIndexFromScroll font un aller-retour', () => { for (const index of [0, 1, 9, 23]) { const top = scrollTopForValueIndex(index, H, LEN) expect(valueIndexFromScroll(top, H, LEN)).toBe(index) } }) it('valueIndexFromScroll boucle en modulo', () => { const top = scrollTopForValueIndex(0, H, LEN) expect(valueIndexFromScroll(top + LEN * H, H, LEN)).toBe(0) }) it('loopCorrection laisse le scroll de la copie du milieu inchangé', () => { const top = scrollTopForValueIndex(12, H, LEN) expect(loopCorrection(top, H, LEN)).toBe(top) }) it('loopCorrection ramène vers le milieu quand on dérive vers le haut', () => { const drifted = scrollTopForValueIndex(0, H, LEN) - LEN * H expect(loopCorrection(drifted, H, LEN)).toBe(drifted + LEN * H) }) it('loopCorrection ramène vers le milieu quand on dérive vers le bas', () => { const drifted = scrollTopForValueIndex(0, H, LEN) + LEN * H expect(loopCorrection(drifted, H, LEN)).toBe(drifted - LEN * H) }) }) function mountWheelHarness(initialIndex: number, onChange: (i: number) => void) { let api!: ReturnType const Harness = defineComponent({ setup() { const container = ref(null) api = useInfiniteWheel(container, { length: 24, itemHeight: 40, initialIndex: () => initialIndex, onChange, }) return {container} }, template: '
', }) const wrapper = mount(Harness, {attachTo: document.body}) return {wrapper, api: () => api} } describe('useInfiniteWheel — composable', () => { it('step(+1) émet l\'index suivant', async () => { const changes: number[] = [] const {api} = mountWheelHarness(9, (i) => changes.push(i)) await nextTick() api().step(1) expect(changes.at(-1)).toBe(10) }) it('step boucle de 23 à 0', async () => { const changes: number[] = [] const {api} = mountWheelHarness(23, (i) => changes.push(i)) await nextTick() api().step(1) expect(changes.at(-1)).toBe(0) }) it('onKeydown ArrowUp décrémente (avec wrap)', async () => { const changes: number[] = [] const {api} = mountWheelHarness(0, (i) => changes.push(i)) await nextTick() api().onKeydown(new KeyboardEvent('keydown', {key: 'ArrowUp'})) expect(changes.at(-1)).toBe(23) }) // Anti-boucle navigateur : un scroll programmatique déclenche une rafale d'évènements // scroll (animation/snap). Ils ne doivent PAS être pris pour du scroll utilisateur, // sinon settle() ré-émet en boucle et corrompt le patch DOM de Vue. it('n\'émet pas en double quand un scroll programmatique déclenche une rafale de scroll', async () => { vi.useFakeTimers() try { const changes: number[] = [] const {wrapper, api} = mountWheelHarness(9, (i) => changes.push(i)) await nextTick() const el = wrapper.element as HTMLElement changes.length = 0 api().scrollToIndex(12) el.dispatchEvent(new Event('scroll')) el.dispatchEvent(new Event('scroll')) el.dispatchEvent(new Event('scroll')) vi.advanceTimersByTime(300) expect(changes).toEqual([12]) } finally { vi.useRealTimers() } }) })