/* eslint-disable max-len */ import { RefObject } from 'react'; import { act, renderHook } from '@testing-library/react'; import useWidgetObserved from '../useWidgetObserved'; import { useStore } from '../../services/store'; jest.mock('../../services/store', () => ({ useStore: jest.fn(), })); describe('useWidgetObserved', () => { let mockObserve: jest.Mock; let mockDisconnect: jest.Mock; let intersectionCallback: IntersectionObserverCallback; let mockTriggerEvent: jest.Mock; let widgetRef: RefObject; beforeEach(() => { jest.useFakeTimers(); jest.clearAllMocks(); mockObserve = jest.fn(); mockDisconnect = jest.fn(); mockTriggerEvent = jest.fn(); widgetRef = { current: document.createElement('div') }; global.IntersectionObserver = jest.fn((callback) => { intersectionCallback = callback; return { observe: mockObserve, disconnect: mockDisconnect, } as unknown as IntersectionObserver; }) as unknown as typeof IntersectionObserver; (useStore as jest.Mock).mockReturnValue({ data: { id: 'widget-default', settings: { type: 'carousel' }, }, triggerEvent: mockTriggerEvent, }); }); afterEach(() => { jest.useRealTimers(); }); it('creates an observer with a full-visibility threshold', () => { renderHook(() => useWidgetObserved(widgetRef)); expect(global.IntersectionObserver).toHaveBeenCalledWith( expect.any(Function), { threshold: 1.0 }, ); expect(mockObserve).toHaveBeenCalledWith(widgetRef.current); }); it('fires widgetObserved after one second of continuous visibility', () => { (useStore as jest.Mock).mockReturnValue({ data: { id: 'widget-visible', settings: { type: 'wall' }, }, triggerEvent: mockTriggerEvent, }); renderHook(() => useWidgetObserved(widgetRef)); act(() => { intersectionCallback([{ isIntersecting: true } as IntersectionObserverEntry], {} as IntersectionObserver); jest.advanceTimersByTime(999); }); expect(mockTriggerEvent).not.toHaveBeenCalled(); act(() => { jest.advanceTimersByTime(1); }); expect(mockTriggerEvent).toHaveBeenCalledWith('widgetObserved', { layout: 'wall' }, 'widget-visible'); }); it('cancels the timer when the widget leaves the viewport before one second', () => { (useStore as jest.Mock).mockReturnValue({ data: { id: 'widget-cancelled', settings: { type: 'mosaic' }, }, triggerEvent: mockTriggerEvent, }); renderHook(() => useWidgetObserved(widgetRef)); act(() => { intersectionCallback([{ isIntersecting: true } as IntersectionObserverEntry], {} as IntersectionObserver); jest.advanceTimersByTime(500); intersectionCallback([{ isIntersecting: false } as IntersectionObserverEntry], {} as IntersectionObserver); jest.advanceTimersByTime(500); }); expect(mockTriggerEvent).not.toHaveBeenCalled(); }); it('fires only once for the same widget id across remounts', () => { (useStore as jest.Mock).mockReturnValue({ data: { id: 'widget-once', settings: { type: 'carousel' }, }, triggerEvent: mockTriggerEvent, }); const { unmount } = renderHook(() => useWidgetObserved(widgetRef)); act(() => { intersectionCallback([{ isIntersecting: true } as IntersectionObserverEntry], {} as IntersectionObserver); jest.advanceTimersByTime(1000); }); unmount(); renderHook(() => useWidgetObserved(widgetRef)); expect(mockTriggerEvent).toHaveBeenCalledTimes(1); expect(mockObserve).toHaveBeenCalledTimes(1); }); it('disconnects the observer and clears a pending timer on unmount', () => { const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); const { unmount } = renderHook(() => useWidgetObserved(widgetRef)); act(() => { intersectionCallback([{ isIntersecting: true } as IntersectionObserverEntry], {} as IntersectionObserver); }); unmount(); expect(mockDisconnect).toHaveBeenCalled(); expect(clearTimeoutSpy).toHaveBeenCalled(); clearTimeoutSpy.mockRestore(); }); });