import { mount, ReactWrapper, shallow } from 'enzyme'; import * as React from 'react'; import InfiniteScroll, { InfiniteScrollProps } from '../InfiniteScroll'; const mockOnLoadMore = jest.fn(); const threshold = 100; const propsList: InfiniteScrollProps = { children: null, isLoading: false, hasMore: false, useWindow: true, onLoadMore: mockOnLoadMore, threshold, throttle: 2, }; describe('components/infinite-scroll/InfiniteScroll', () => { const items = new Array(20).fill('ITEM'); let attachTo: HTMLDivElement; let component: ReactWrapper; const getSentinel = () => { return component?.find('[data-testid="sentinel"]').getDOMNode(); }; beforeEach(() => { const container = document.createElement('div'); document.body.appendChild(container); attachTo = container; // Element initializes with sentinel above threshold (no initial load). // Without this, top will always be 0 due to jest limitation. jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: window.innerHeight + (threshold + 1), } as DOMRect); }); afterEach(() => { component?.unmount(); document.body.innerHTML = ''; document.head.innerHTML = ''; jest.restoreAllMocks(); }); it('should render with default props', () => { const wrapper = shallow(); expect(wrapper).toMatchInlineSnapshot(`
`); }); it('should render sentinel to calculate scroll position', () => { const wrapper = shallow(); expect(wrapper.find('[data-testid="sentinel"]').length).toEqual(1); }); describe('using window', () => { beforeEach(() => { component = mount(
{items.map((item, i) => (
{item}
))}
, { attachTo }, ); }); it('should call onLoadMore if sentinel is in threshold range and window is not scrollable', () => { const sentinel = getSentinel(); sentinel.getBoundingClientRect = jest .fn() .mockReturnValue({ top: window.innerHeight + (threshold - 1) } as DOMRect); // update prop to trigger useEffect component.setProps({ throttle: 1 }); expect(mockOnLoadMore).toBeCalledTimes(1); }); it('should call onLoadMore if sentinel is in threshold range while scrolling in window', () => { const sentinel = getSentinel(); sentinel.getBoundingClientRect = jest .fn() .mockReturnValue({ top: window.innerHeight + (threshold - 1) } as DOMRect); window.dispatchEvent(new Event('scroll')); expect(mockOnLoadMore).toBeCalledTimes(1); }); it('should not call onLoadMore if sentinel is not in threshold range while scrolling in window', () => { window.dispatchEvent(new Event('scroll')); expect(mockOnLoadMore).toBeCalledTimes(0); }); }); describe('using scrollContainerNode', () => { let scrollContainer: HTMLDivElement; beforeEach(() => { scrollContainer = document.createElement('div'); scrollContainer.getBoundingClientRect = jest.fn().mockReturnValue({ bottom: 500 } as DOMRect); component = mount(
{items.map((item, i) => (
{item}
))}
, { attachTo }, ); }); it('should call onLoadMore if sentinel is in threshold range and window is not scrollable', () => { const sentinel = getSentinel(); sentinel.getBoundingClientRect = jest.fn().mockReturnValue({ top: 500 + (threshold - 1) } as DOMRect); // update prop to trigger useEffect component.setProps({ throttle: 1 }); expect(mockOnLoadMore).toBeCalledTimes(1); }); it('should call onLoadMore if sentinel is in threshold range while scrolling scrollContainerNode', () => { const sentinel = getSentinel(); sentinel.getBoundingClientRect = jest.fn().mockReturnValue({ top: 500 + (threshold - 1) } as DOMRect); scrollContainer.dispatchEvent(new Event('scroll')); expect(mockOnLoadMore).toBeCalledTimes(1); }); it('should not call onLoadMore if sentinel is not in threshold range while scrolling scrollContainerNode', () => { scrollContainer.dispatchEvent(new Event('scroll')); expect(mockOnLoadMore).not.toBeCalled(); }); }); describe('with sentinel in range', () => { beforeEach(() => { component = mount(
{items.map((item, i) => (
{item}
))}
, { attachTo }, ); const sentinel = getSentinel(); sentinel.getBoundingClientRect = jest .fn() .mockReturnValue({ top: window.innerHeight + (threshold - 1) } as DOMRect); jest.resetAllMocks(); }); it('should not call onLoadMore if isLoading', () => { component.setProps({ isLoading: true }); window.dispatchEvent(new Event('scroll')); expect(mockOnLoadMore).not.toBeCalled(); }); it('should not call onLoadMore if !hasMore', () => { component.setProps({ hasMore: false }); window.dispatchEvent(new Event('scroll')); expect(mockOnLoadMore).not.toBeCalled(); }); }); describe('event handlers', () => { function assertScrollAndResizeEvents(spyInstance: jest.SpyInstance, numberOfCalls = 1) { // there are a lot of 'error' event listeners, we're only interested in scroll and resize const scrollEvents = spyInstance.mock.calls.filter(([event]) => event === 'scroll'); const resizeEvents = spyInstance.mock.calls.filter(([event]) => event === 'resize'); expect(scrollEvents).toHaveLength(numberOfCalls); expect(resizeEvents).toHaveLength(numberOfCalls); } it('should check if listeners are added and removed', () => { const addEventListenerWindow = jest.spyOn(window, 'addEventListener'); const removeEventListenerWindow = jest.spyOn(window, 'removeEventListener'); const scrollContainerNode = document.createElement('div'); const addEventListenerScrollContainer = jest.spyOn(scrollContainerNode, 'addEventListener'); const removeEventListenerScrollContainer = jest.spyOn(scrollContainerNode, 'removeEventListener'); assertScrollAndResizeEvents(addEventListenerWindow, 0); assertScrollAndResizeEvents(removeEventListenerWindow, 0); assertScrollAndResizeEvents(addEventListenerScrollContainer, 0); assertScrollAndResizeEvents(removeEventListenerScrollContainer, 0); component = mount(); assertScrollAndResizeEvents(addEventListenerWindow); component.setProps({ useWindow: false }); assertScrollAndResizeEvents(removeEventListenerWindow); component.setProps({ scrollContainerNode, }); assertScrollAndResizeEvents(addEventListenerScrollContainer); component.setProps({ useWindow: true, }); assertScrollAndResizeEvents(removeEventListenerScrollContainer); // we should have another pair of addEventsListeners on window assertScrollAndResizeEvents(addEventListenerWindow, 2); }); }); });