diff --git a/packages/venia-concept/src/RootComponents/Category/category.js b/packages/venia-concept/src/RootComponents/Category/category.js index 4f1b8e0d88..bbd6a71d0b 100644 --- a/packages/venia-concept/src/RootComponents/Category/category.js +++ b/packages/venia-concept/src/RootComponents/Category/category.js @@ -57,6 +57,13 @@ class Category extends Component { id: 3 }; + componentDidUpdate(prevProps) { + // If the current page has changed, scroll back up to the top. + if (this.props.currentPage !== prevProps.currentPage) { + window.scrollTo(0, 0); + } + } + render() { const { id, diff --git a/packages/venia-concept/src/actions/catalog/__tests__/actions.spec.js b/packages/venia-concept/src/actions/catalog/__tests__/actions.spec.js new file mode 100644 index 0000000000..19bb797864 --- /dev/null +++ b/packages/venia-concept/src/actions/catalog/__tests__/actions.spec.js @@ -0,0 +1,154 @@ +import actions from '../actions'; + +const MOCK_PAYLOAD = 'Unit Test Payload'; +const ERROR = new Error('Unit Test'); + +describe('getAllCategories', () => { + const PREFIX = 'CATALOG/GET_ALL_CATEGORIES'; + + describe('REQUEST', () => { + const EXPECTED_NAME = `${PREFIX}/REQUEST`; + + test('it returns the proper action type', () => { + expect(actions.getAllCategories.request.toString()).toBe( + EXPECTED_NAME + ); + }); + + test('it returns a proper action object', () => { + expect(actions.getAllCategories.request(MOCK_PAYLOAD)).toEqual({ + type: EXPECTED_NAME, + payload: MOCK_PAYLOAD + }); + + expect(actions.getAllCategories.request(ERROR)).toEqual({ + type: EXPECTED_NAME, + payload: ERROR, + error: true + }); + }); + }); + + describe('RECEIVE', () => { + const EXPECTED_NAME = `${PREFIX}/RECEIVE`; + + test('it returns the proper action type', () => { + expect(actions.getAllCategories.receive.toString()).toBe( + EXPECTED_NAME + ); + }); + + test('it returns a proper action object', () => { + expect(actions.getAllCategories.receive(MOCK_PAYLOAD)).toEqual({ + type: EXPECTED_NAME, + payload: MOCK_PAYLOAD + }); + + expect(actions.getAllCategories.receive(ERROR)).toEqual({ + type: EXPECTED_NAME, + payload: ERROR, + error: true + }); + }); + }); +}); + +describe('setCurrentPage', () => { + const PREFIX = 'CATALOG/SET_CURRENT_PAGE'; + + describe('REQUEST', () => { + const EXPECTED_NAME = `${PREFIX}/REQUEST`; + + test('it returns the proper action type', () => { + expect(actions.setCurrentPage.request.toString()).toBe( + EXPECTED_NAME + ); + }); + + test('it returns a proper action object', () => { + expect(actions.setCurrentPage.request(MOCK_PAYLOAD)).toEqual({ + type: EXPECTED_NAME, + payload: MOCK_PAYLOAD + }); + + expect(actions.setCurrentPage.request(ERROR)).toEqual({ + type: EXPECTED_NAME, + payload: ERROR, + error: true + }); + }); + }); + + describe('RECEIVE', () => { + const EXPECTED_NAME = `${PREFIX}/RECEIVE`; + + test('it returns the proper action type', () => { + expect(actions.setCurrentPage.receive.toString()).toBe( + EXPECTED_NAME + ); + }); + + test('it returns a proper action object', () => { + expect(actions.setCurrentPage.receive(MOCK_PAYLOAD)).toEqual({ + type: EXPECTED_NAME, + payload: MOCK_PAYLOAD + }); + + expect(actions.setCurrentPage.receive(ERROR)).toEqual({ + type: EXPECTED_NAME, + payload: ERROR, + error: true + }); + }); + }); +}); + +describe('setPrevPageTotal', () => { + const PREFIX = 'CATALOG/SET_PREV_PAGE_TOTAL'; + + describe('REQUEST', () => { + const EXPECTED_NAME = `${PREFIX}/REQUEST`; + + test('it returns the proper action type', () => { + expect(actions.setPrevPageTotal.request.toString()).toBe( + EXPECTED_NAME + ); + }); + + test('it returns a proper action object', () => { + expect(actions.setPrevPageTotal.request(MOCK_PAYLOAD)).toEqual({ + type: EXPECTED_NAME, + payload: MOCK_PAYLOAD + }); + + expect(actions.setPrevPageTotal.request(ERROR)).toEqual({ + type: EXPECTED_NAME, + payload: ERROR, + error: true + }); + }); + }); + + describe('RECEIVE', () => { + const EXPECTED_NAME = `${PREFIX}/RECEIVE`; + + test('it returns the proper action type', () => { + expect(actions.setPrevPageTotal.receive.toString()).toBe( + EXPECTED_NAME + ); + }); + + test('it returns a proper action object', () => { + expect(actions.setPrevPageTotal.receive(MOCK_PAYLOAD)).toEqual({ + type: EXPECTED_NAME, + payload: MOCK_PAYLOAD + }); + + expect(actions.setPrevPageTotal.receive(ERROR)).toEqual({ + type: EXPECTED_NAME, + payload: ERROR, + error: true + }); + }); + }); +}); diff --git a/packages/venia-concept/src/actions/catalog/__tests__/asyncActions.spec.js b/packages/venia-concept/src/actions/catalog/__tests__/asyncActions.spec.js new file mode 100644 index 0000000000..5256ac90da --- /dev/null +++ b/packages/venia-concept/src/actions/catalog/__tests__/asyncActions.spec.js @@ -0,0 +1,90 @@ +import { dispatch, getState } from 'src/store'; +import actions from '../actions'; +import mockData from '../mockData'; +import { + getAllCategories, + setCurrentPage, + setPrevPageTotal +} from '../asyncActions'; + +jest.mock('src/store'); + +const thunkArgs = [dispatch, getState]; + +afterEach(() => { + dispatch.mockClear(); +}); + +describe('getAllCategories', () => { + test('it returns a thunk', () => { + expect(getAllCategories()).toBeInstanceOf(Function); + }); + + test('its thunk returns undefined', async () => { + const result = await getAllCategories()(...thunkArgs); + + expect(result).toBeUndefined(); + }); + + test('its thunk dispatches actions', async () => { + await getAllCategories()(...thunkArgs); + + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith( + 1, + actions.getAllCategories.request() + ); + expect(dispatch).toHaveBeenNthCalledWith( + 2, + actions.getAllCategories.receive(mockData) + ); + }); +}); + +describe('setCurrentPage', () => { + const PAYLOAD = 2; + + test('it returns a thunk', () => { + expect(setCurrentPage(PAYLOAD)).toBeInstanceOf(Function); + }); + + test('its thunk returns undefined', async () => { + const result = await setCurrentPage(PAYLOAD)(...thunkArgs); + + expect(result).toBeUndefined(); + }); + + test('its thunk dispatches actions', async () => { + await setCurrentPage(PAYLOAD)(...thunkArgs); + + expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenNthCalledWith( + 1, + actions.setCurrentPage.receive(PAYLOAD) + ); + }); +}); + +describe('setPrevPageTotal', () => { + const PAYLOAD = 10; + + test('it returns a thunk', () => { + expect(setPrevPageTotal(PAYLOAD)).toBeInstanceOf(Function); + }); + + test('its thunk returns undefined', async () => { + const result = await setPrevPageTotal(PAYLOAD)(...thunkArgs); + + expect(result).toBeUndefined(); + }); + + test('its thunk dispatches actions', async () => { + await setPrevPageTotal(PAYLOAD)(...thunkArgs); + + expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenNthCalledWith( + 1, + actions.setPrevPageTotal.receive(PAYLOAD) + ); + }); +}); diff --git a/packages/venia-concept/src/actions/catalog/asyncActions.js b/packages/venia-concept/src/actions/catalog/asyncActions.js index 16c973de7b..1ec345d88e 100644 --- a/packages/venia-concept/src/actions/catalog/asyncActions.js +++ b/packages/venia-concept/src/actions/catalog/asyncActions.js @@ -18,7 +18,6 @@ export const getAllCategories = () => export const setCurrentPage = payload => async function thunk(dispatch) { dispatch(actions.setCurrentPage.receive(payload)); - window.scrollTo(0, 0); }; export const setPrevPageTotal = payload => diff --git a/packages/venia-concept/src/components/Pagination/pagination.js b/packages/venia-concept/src/components/Pagination/pagination.js index b14b5e2b53..1d11bd6d1a 100644 --- a/packages/venia-concept/src/components/Pagination/pagination.js +++ b/packages/venia-concept/src/components/Pagination/pagination.js @@ -167,6 +167,10 @@ class Pagination extends Component { const queryPage = Math.max( 1, + // Note: The ~ operator is a bitwise NOT operator. + // Bitwise NOTing any number x yields -(x + 1). For example, ~-5 yields 4. + // Importantly, it truncates any fractional component of x. For example, ~-5.7 also yields 4. + // For positive numbers, applying this operator twice has the same effect as Math.floor. ~~getQueryParameterValue({ location, queryParameter: 'page'