32
loading...
This website collects cookies to deliver better user experience
import {renderHook, act} from '@testing-library/react-hooks';
import usePagination from './UsePagination';
describe('UsePagination hook', () => {
it('should exist', () => {
const result = usePagination();
expect(result).toBeDefined();
});
});
const usePagination = () => {
return {};
};
export default usePagination;
it('should throw if no total pages were given to it', () => {
expect(() => {
usePagination();
}).toThrow('The UsePagination hook must receive a totalPages argument for it to work');
});
const usePagination = ({totalPages} = {}) => {
if (!totalPages) {
throw new Error('The UsePagination hook must receive a totalPages argument for it to work');
}
return {};
};
export default usePagination;
it('should exist', () => {
const result = usePagination({totalPages: 10});
expect(result).toBeDefined();
});
export const NO_TOTAL_PAGES_ERROR = 'The UsePagination hook must receive a totalPages argument for it to work';
const usePagination = ({totalPages} = {}) => {
if (!totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
return {};
};
export default usePagination;
import usePagination, {NO_TOTAL_PAGES_ERROR} from './UsePagination';
describe('UsePagination hook', () => {
it('should exist', () => {
const result = usePagination({totalPages: 10});
expect(result).toBeDefined();
});
it('should throw if no total pages were given to it', () => {
expect(() => {
usePagination();
}).toThrow(NO_TOTAL_PAGES_ERROR);
});
});
it('should return the totalPages that was given to it', () => {
const {result} = renderHook(() => usePagination({totalPages: 10}));
expect(result.current.totalPages).toEqual(10);
});
const usePagination = ({totalPages} = {}) => {
if (!totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
return {totalPages};
};
NOTE: I jumped a step here since the minimal code to satisfy the test would be returning a hard coded 10 as the totalPages, but it is redundant in this case since the logic here is really straightforward.
it('should return 0 as the cursor position if no cursor was given to it
', () => {
const {result} = renderHook(() => usePagination({totalPages: 10}));
expect(result.current.cursor).toEqual(0);
});
const usePagination = ({totalPages} = {}) => {
if (!totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
return {totalPages, cursor: 0};
};
it('should return the received cursor position if it was given to it', () => {
const {result} = renderHook(() => usePagination({totalPages: 10, cursor: 5}));
expect(result.current.cursor).toEqual(5);
});
const usePagination = ({totalPages, cursor} = {}) => {
if (!totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
cursor = cursor || 0;
return {totalPages, cursor};
};
it('should return the hooks methods', () => {
const {result} = renderHook(() => usePagination({totalPages: 10}));
expect(typeof result.current.goNext).toEqual('function');
expect(typeof result.current.goPrev).toEqual('function');
expect(typeof result.current.setCursor).toEqual('function');
});
const usePagination = ({totalPages, cursor} = {}) => {
if (!totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
cursor = cursor || 0;
const goNext = () => {};
const goPrev = () => {};
const setCursor = () => {};
return {totalPages, cursor, goNext, goPrev, setCursor};
};
describe('setCursor method', () => {
it('should set the hooks cursor to the given value
', () => {
const {result} = renderHook(() => usePagination({totalPages: 10}));
act(() => {
result.current.setCursor(4);
});
expect(result.current.cursor).toEqual(4);
});
});
NOTE: I created it in a nested “describe” for better order and readability.
const usePagination = ({totalPages, initialCursor} = {}) => {
if (!totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
const [cursor, setCursor] = useState(initialCursor || 0);
const goNext = () => {};
const goPrev = () => {};
return {totalPages, cursor, goNext, goPrev, setCursor};
};
it('should exist', () => {
const {result} = renderHook(() => usePagination({totalPages: 10}));
expect(result.current).toBeDefined();
});
it('should not set the hooks cursor if the given value is above the total pages', () => {
const {result} = renderHook(() => usePagination({totalPages: 10}));
act(() => {
result.current.setCursor(15);
});
expect(result.current.cursor).toEqual(0);
});
it('should not set the hooks cursor if the given value is lower than 0', () => {
const {result} = renderHook(() => usePagination({totalPages: 10}));
act(() => {
result.current.setCursor(-3);
});
expect(result.current.cursor).toEqual(0);
});
const SET_CURSOR_ACTION = 'setCursorAction';
...
const [cursor, dispatch] = useReducer(reducer, initialCursor || 0);
const setCursor = (value) => {
dispatch({value, totalPages});
};
function reducer(state, action) {
let result = state;
if (action.value > 0 && action.value < action.totalPages) {
result = action.value;
}
return result;
}
describe('goNext method', () => {
it('should set the hooks cursor to the next value', () => {
const {result} = renderHook(() => usePagination({totalPages: 2}));
act(() => {
result.current.goNext();
});
expect(result.current.cursor).toEqual(1);
});
});
const goNext = () => {
const nextCursor = cursor + 1;
setCursor(nextCursor);
};
it('should not set the hooks cursor to the next value if we reached the last page', () => {
const {result} = renderHook(() => usePagination({totalPages: 5, initialCursor: 4}));
act(() => {
result.current.goNext();
});
expect(result.current.cursor).toEqual(4);
});
describe('onChange callback handler', () => {
it('should be invoked when the cursor changes by setCursor method', () => {
const onChangeSpy = jest.fn();
const {result} = renderHook(() => usePagination({totalPages: 5, onChange: onChangeSpy}));
act(() => {
result.current.setCursor(3);
});
expect(onChangeSpy).toHaveBeenCalledWith(3);
});
});
useEffect(() => {
onChange?.(cursor);
}, [cursor]);
it('should not be invoked when the hook is initialized', () => {
const onChangeSpy = jest.fn();
renderHook(() => usePagination({totalPages: 5, onChange: onChangeSpy}));
expect(onChangeSpy).not.toHaveBeenCalled();
});
const isHookInitializing = useRef(true);
useEffect(() => {
if (isHookInitializing.current) {
isHookInitializing.current = false;
} else {
onChange?.(cursor);
}
}, [cursor]);
Challenge yourself - see if you can implement a cyclic mode for the pagination (for instance, once it reaches the end it goes back to the beginning) using TDD 🤓
import {useEffect, useReducer, useRef, useState} from 'react';
export const NO_TOTAL_PAGES_ERROR = 'The UsePagination hook must receive a totalPages argument for it to work';
const usePagination = ({totalPages, initialCursor, onChange} = {}) => {
if (!totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
const [cursor, dispatch] = useReducer(reducer, initialCursor || 0);
const setCursor = (value) => {
dispatch({value, totalPages});
};
const goNext = () => {
const nextCursor = cursor + 1;
setCursor(nextCursor);
};
const goPrev = () => {
const prevCursor = cursor - 1;
setCursor(prevCursor);
};
const isHookInitializing = useRef(true);
useEffect(() => {
if (isHookInitializing.current) {
isHookInitializing.current = false;
} else {
onChange?.(cursor);
}
}, [cursor]);
return {totalPages, cursor, goNext, goPrev, setCursor};
};
function reducer(state, action) {
let result = state;
if (action.value > 0 && action.value < action.totalPages) {
result = action.value;
}
return result;
}
export default usePagination;