/** * @jest-environment jsdom * @jest-environment-options {"url": "https://www.ft.com/"} */ import { mockCarouselEnterViewPortEvent, mockCarouselExitViewPortEvent, mockCarouselSwipeEvent, } from './mock-tracking-events' import { CarouselTrackingClient } from './tracking' const createTouchEvent = ( type: 'touchstart' | 'touchend', clientX: number, clientY: number ): TouchEvent => { const event = new Event(type, { bubbles: true }) as TouchEvent Object.defineProperty(event, 'changedTouches', { value: [{ clientX, clientY }], }) return event } describe('Carousel Tracking Client', () => { let mockComponent: HTMLElement let dispatchEventSpy: jest.SpyInstance let mockDisconnectIntersection: jest.Mock let mockIntersectionObserver: jest.Mock beforeEach(() => { mockComponent = document.createElement('div') mockComponent.setAttribute('data-component-id', '123') mockDisconnectIntersection = jest.fn() mockIntersectionObserver = jest.fn(() => { return { observe: jest.fn(), unobserve: jest.fn(), disconnect: mockDisconnectIntersection, } }) window.IntersectionObserver = mockIntersectionObserver document.body.appendChild(mockComponent) jest.useFakeTimers() dispatchEventSpy = jest.spyOn(document.body, 'dispatchEvent') }) afterEach(() => { jest.clearAllMocks() jest.useRealTimers() document.body.removeChild(mockComponent) }) describe('initialisation and teardown', () => { it('should initialise and teardown', () => { new CarouselTrackingClient(mockComponent) expect(mockIntersectionObserver).toHaveBeenCalledTimes(1) }) it('should teardown the instance on destroy', () => { const tracker = new CarouselTrackingClient(mockComponent) tracker.destroy() expect(mockDisconnectIntersection).toHaveBeenCalledTimes(1) }) }) describe('component visibility changes', () => { it('should send a view event when the component enters and exits the viewport', () => { jest.setSystemTime(new Date('2026-05-27T10:00:00Z')) const tracker = new CarouselTrackingClient(mockComponent) const mockEnterView: IntersectionObserverEntry[] = [ { target: mockComponent, isIntersecting: true, intersectionRatio: 1.0, boundingClientRect: { height: 100, } as unknown as DOMRectReadOnly, intersectionRect: {} as DOMRectReadOnly, rootBounds: null, time: 1, }, ] tracker.onChange(mockEnterView) const viewEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent expect(viewEvent.type).toBe('oTracking.event') expect(viewEvent.detail).toEqual(mockCarouselEnterViewPortEvent) const mockExitView: IntersectionObserverEntry[] = [ { target: mockComponent, isIntersecting: false, intersectionRatio: 0, boundingClientRect: { height: 100, } as unknown as DOMRectReadOnly, intersectionRect: {} as DOMRectReadOnly, rootBounds: null, time: 2341, }, ] jest.setSystemTime(new Date('2026-05-27T10:00:01Z')) tracker.onChange(mockExitView) const exitEvent = dispatchEventSpy.mock.calls[1][0] as CustomEvent expect(exitEvent.type).toBe('oTracking.event') expect(exitEvent.detail).toEqual(mockCarouselExitViewPortEvent) }) }) describe('component interactions', () => { it('should send a swipe event when the component is swiped horizontally', () => { new CarouselTrackingClient(mockComponent) mockComponent.dispatchEvent(createTouchEvent('touchstart', 100, 50)) mockComponent.dispatchEvent(createTouchEvent('touchend', 40, 55)) expect(dispatchEventSpy).toHaveBeenCalledTimes(1) const swipeEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent expect(swipeEvent.type).toBe('oTracking.event') expect(swipeEvent.detail).toEqual(mockCarouselSwipeEvent) }) it('should keep listening after a touch that is not a swipe', () => { new CarouselTrackingClient(mockComponent) mockComponent.dispatchEvent(createTouchEvent('touchstart', 100, 50)) mockComponent.dispatchEvent(createTouchEvent('touchend', 100, 50)) expect(dispatchEventSpy).not.toHaveBeenCalled() mockComponent.dispatchEvent(createTouchEvent('touchstart', 100, 50)) mockComponent.dispatchEvent(createTouchEvent('touchend', 40, 55)) expect(dispatchEventSpy).toHaveBeenCalledTimes(1) const swipeEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent expect(swipeEvent.detail).toEqual(mockCarouselSwipeEvent) }) it('should not send a swipe event for vertical movement', () => { new CarouselTrackingClient(mockComponent) mockComponent.dispatchEvent(createTouchEvent('touchstart', 100, 50)) mockComponent.dispatchEvent(createTouchEvent('touchend', 90, 120)) expect(dispatchEventSpy).not.toHaveBeenCalled() }) it('should send a swipe event when the component is scrolled horizontally', () => { new CarouselTrackingClient(mockComponent) mockComponent.scrollLeft = 40 mockComponent.dispatchEvent(new Event('scroll')) expect(dispatchEventSpy).toHaveBeenCalledTimes(1) const swipeEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent expect(swipeEvent.type).toBe('oTracking.event') expect(swipeEvent.detail).toEqual(mockCarouselSwipeEvent) }) it('should only send one swipe event for horizontal scroll interactions', () => { new CarouselTrackingClient(mockComponent) mockComponent.scrollLeft = 40 mockComponent.dispatchEvent(new Event('scroll')) mockComponent.scrollLeft = 80 mockComponent.dispatchEvent(new Event('scroll')) expect(dispatchEventSpy).toHaveBeenCalledTimes(1) }) it('should send a swipe event for horizontal wheel gestures', () => { new CarouselTrackingClient(mockComponent) mockComponent.dispatchEvent(new WheelEvent('wheel', { deltaX: 40 })) expect(dispatchEventSpy).toHaveBeenCalledTimes(1) const swipeEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent expect(swipeEvent.type).toBe('oTracking.event') expect(swipeEvent.detail).toEqual(mockCarouselSwipeEvent) }) it('should not send a swipe event for vertical wheel gestures', () => { new CarouselTrackingClient(mockComponent) mockComponent.dispatchEvent(new WheelEvent('wheel', { deltaY: 40 })) expect(dispatchEventSpy).not.toHaveBeenCalled() }) it('should send a swipe event for horizontal mouse drag', () => { new CarouselTrackingClient(mockComponent) mockComponent.dispatchEvent( new MouseEvent('mousedown', { clientX: 150, clientY: 80 }) ) mockComponent.dispatchEvent( new MouseEvent('mouseup', { clientX: 110, clientY: 85 }) ) expect(dispatchEventSpy).toHaveBeenCalledTimes(1) const swipeEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent expect(swipeEvent.type).toBe('oTracking.event') expect(swipeEvent.detail).toEqual(mockCarouselSwipeEvent) }) it('should not send a swipe event for vertical mouse drag', () => { new CarouselTrackingClient(mockComponent) mockComponent.dispatchEvent( new MouseEvent('mousedown', { clientX: 120, clientY: 80 }) ) mockComponent.dispatchEvent( new MouseEvent('mouseup', { clientX: 115, clientY: 120 }) ) expect(dispatchEventSpy).not.toHaveBeenCalled() }) }) })