import { describe, it, expect } from 'vitest'; import { ref, type Ref } from 'vue'; import { useDragHandler, type DragDomModifier } from '@/presentation/components/Scripts/Slider/UseDragHandler'; import { ThrottleStub } from '@tests/unit/shared/Stubs/ThrottleStub'; import { type ThrottleFunction } from '@/application/Common/Timing/Throttle'; import type { ConstructorArguments } from '@/TypeHelpers'; import type { LifecycleHook } from '@/presentation/components/Shared/Hooks/Common/LifecycleHook'; import { LifecycleHookStub } from '@tests/unit/shared/Stubs/LifecycleHookStub'; describe('useDragHandler', () => { describe('initially', () => { it('sets displacement X to 0', () => { // arrange const expectedValue = 0; // act const { displacementX } = initializeDragHandlerWithMocks(); // assert expect(displacementX.value).to.equal(expectedValue); }); it('sets dragging state to false', () => { // arrange const expectedValue = false; // act const { isDragging } = initializeDragHandlerWithMocks(); // assert expect(isDragging.value).to.equal(expectedValue); }); it('disables element touch action', () => { // arrange const expectedTouchAction = 'none'; const mockElement = document.createElement('div'); // act initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), }); // assert const actualTouchAction = mockElement.style.touchAction; expect(actualTouchAction).to.equal(expectedTouchAction); }); it('attaches event listener for drag start', () => { // arrange const expectedEventName: keyof HTMLElementEventMap = 'pointerdown'; let actualEventName: keyof HTMLElementEventMap | undefined; const mockElement = document.createElement('div'); mockElement.addEventListener = (eventName: keyof HTMLElementEventMap) => { actualEventName = eventName; }; // act initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), }); // assert expect(expectedEventName).to.equal(actualEventName); }); }); describe('on drag', () => { it('activates dragging on start of drag', () => { // arrange const expectedDragState = true; const mockElement = document.createElement('div'); // act const { isDragging } = initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), }); mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 })); // assert expect(isDragging.value).to.equal(expectedDragState); }); it('updates displacement during dragging', () => { // arrange const initialDragX = 100; const finalDragX = 150; const expectedDisplacementX = finalDragX - initialDragX; const mockElement = document.createElement('div'); const dragDomModifierMock = createDragDomModifierMock(); // act const { displacementX } = initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), dragDomModifier: dragDomModifierMock, }); mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 })); dragDomModifierMock.simulateEvent('pointermove', createMockPointerEvent('pointermove', { clientX: finalDragX })); // assert expect(displacementX.value).to.equal(expectedDisplacementX); }); it('attaches event listeners', () => { // arrange const expectedEventNames: ReadonlyArray = [ 'pointerup', 'pointermove', ]; const mockElement = document.createElement('div'); const dragDomModifierMock = createDragDomModifierMock(); // act initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), dragDomModifier: dragDomModifierMock, }); mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 })); // assert const actualEventNames = [...dragDomModifierMock.events].map(([eventName]) => eventName); expect(expectedEventNames).to.have.lengthOf(actualEventNames.length); expect(expectedEventNames).to.have.members(actualEventNames); }); describe('throttling', () => { it('initializes event throttling', () => { // arrange const throttleStub = new ThrottleStub() .withImmediateExecution(false); const mockElement = document.createElement('div'); // act initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), throttler: throttleStub.func, }); // assert expect(throttleStub.throttleInitializationCallArgs.length).to.equal(1); }); it('sets a specific throttle time interval', () => { // arrange const expectedThrottleInMs = 15; const throttleStub = new ThrottleStub() .withImmediateExecution(false); const mockElement = document.createElement('div'); // act initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), throttler: throttleStub.func, }); // assert expect(throttleStub.throttleInitializationCallArgs.length).to.equal(1); const [, actualThrottleInMs] = throttleStub.throttleInitializationCallArgs[0]; expect(expectedThrottleInMs).to.equal(actualThrottleInMs); }); it('limits frequency of drag movement updates', () => { // arrange const expectedTotalThrottledEvents = 3; const throttleStub = new ThrottleStub(); const mockElement = document.createElement('div'); const dragDomModifierMock = createDragDomModifierMock(); // act initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), throttler: throttleStub.func, dragDomModifier: dragDomModifierMock, }); mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 })); for (let i = 0; i < expectedTotalThrottledEvents; i++) { dragDomModifierMock.simulateEvent('pointermove', createMockPointerEvent('pointermove', { clientX: 110 })); } // assert const actualTotalThrottledEvents = throttleStub.throttledFunctionCallArgs.length; expect(actualTotalThrottledEvents).to.equal(expectedTotalThrottledEvents); }); it('calculates displacement considering throttling', () => { // arrange const throttleStub = new ThrottleStub().withImmediateExecution(true); const mockElement = document.createElement('div'); const initialDragX = 100; const firstDisplacementX = 10; const dragDomModifierMock = createDragDomModifierMock(); // act const { displacementX } = initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), throttler: throttleStub.func, dragDomModifier: dragDomModifierMock, }); mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: initialDragX })); dragDomModifierMock.simulateEvent('pointermove', createMockPointerEvent('pointermove', { clientX: initialDragX - firstDisplacementX })); dragDomModifierMock.simulateEvent('pointermove', createMockPointerEvent('pointermove', { clientX: initialDragX - firstDisplacementX * 2 })); throttleStub.executeFirst(); // assert expect(displacementX.value).to.equal(firstDisplacementX); }); }); }); describe('on drag end', () => { it('deactivates dragging on drag end', () => { // arrange const expectedDraggingState = false; const mockElement = document.createElement('div'); const dragDomModifierMock = createDragDomModifierMock(); // act const { isDragging } = initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), dragDomModifier: dragDomModifierMock, }); mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 })); dragDomModifierMock.simulateEvent('pointerup', createMockPointerEvent('pointerup')); // assert expect(isDragging.value).to.equal(expectedDraggingState); }); it('removes event listeners', () => { // arrange const mockElement = document.createElement('div'); const dragDomModifierMock = createDragDomModifierMock(); // act initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), dragDomModifier: dragDomModifierMock, }); mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 })); dragDomModifierMock.simulateEvent('pointerup', createMockPointerEvent('pointerup')); // assert const actualEvents = [...dragDomModifierMock.events]; expect(actualEvents).to.have.lengthOf(0); }); }); describe('on teardown', () => { it('removes event listeners', () => { // arrange const teardownHook = new LifecycleHookStub(); const mockElement = document.createElement('div'); const dragDomModifierMock = createDragDomModifierMock(); // act initializeDragHandlerWithMocks({ draggableElementRef: ref(mockElement), dragDomModifier: dragDomModifierMock, onTeardown: teardownHook.getHook(), }); mockElement.dispatchEvent(createMockPointerEvent('pointerdown', { clientX: 100 })); teardownHook.executeAllCallbacks(); // assert const actualEvents = [...dragDomModifierMock.events]; expect(actualEvents).to.have.lengthOf(0); }); }); }); function initializeDragHandlerWithMocks(mocks?: { readonly dragDomModifier?: DragDomModifier; readonly draggableElementRef?: Ref; readonly throttler?: ThrottleFunction, readonly onTeardown?: LifecycleHook, }) { return useDragHandler( mocks?.draggableElementRef ?? ref(document.createElement('div')), mocks?.dragDomModifier ?? createDragDomModifierMock(), mocks?.throttler ?? new ThrottleStub().withImmediateExecution(true).func, mocks?.onTeardown ?? new LifecycleHookStub().getHook(), ); } function createMockPointerEvent(...args: ConstructorArguments): PointerEvent { return new MouseEvent(...args) as PointerEvent; // jsdom does not support `PointerEvent` constructor, https://github.com/jsdom/jsdom/issues/2527 } function createDragDomModifierMock(): DragDomModifier & { simulateEvent(type: keyof DocumentEventMap, event: Event): void; readonly events: Map; } { const events = new Map(); return { addEventListenerToDocument: (type, handler) => { events.set(type, handler); }, removeEventListenerFromDocument: (type) => { events.delete(type); }, simulateEvent: (type, event) => { const handler = events.get(type); if (!handler) { throw new Error(`No event handler registered for: ${type}`); } handler(event); }, events, }; }