Skip to content

Commit

Permalink
refactor!: change Pagination from default to named export
Browse files Browse the repository at this point in the history
feat: expose paginationStateReducer
test: add 100% coverage
refactor!: when page size or total items is changed and a target page is not specified, the current page is left as is instead of changing to 0
BREAKING CHANGE: flowtypes are no exported
  • Loading branch information
erictooth committed Jun 23, 2021
1 parent 8fc6566 commit cfdd6ec
Show file tree
Hide file tree
Showing 10 changed files with 377 additions and 118 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -14,6 +14,7 @@
<a href="https://prettier.io">
<img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg" alt="Code Style: Prettier">
</a>
<img src="https://img.shields.io/badge/coverage-100%25-brightgreen.svg" alt="100% coverage">
<a href="https://bundlephobia.com/package/react-use-pagination@latest">
<img src="https://badgen.net/bundlephobia/minzip/react-use-pagination@latest">
</a>
Expand Down
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -44,12 +44,14 @@
"@erictooth/eslint-config": "^3",
"@erictooth/prettier-config": "^4",
"@erictooth/semantic-release-npm-github-config": "^1",
"@testing-library/react-hooks": "^7.0.0",
"@types/jest": "^26",
"@types/react": "^17",
"esbuild": "^0.12.9",
"eslint": "^7",
"jest": "^27",
"prettier": "^2",
"react-test-renderer": "^17.0.2",
"ts-jest": "^27",
"typescript": "^4"
},
Expand All @@ -70,7 +72,7 @@
"jest": {
"preset": "ts-jest",
"collectCoverageFrom": [
"src/**/*.{ts, tsx}"
"src/**/{!(index),}.{ts, tsx}"
]
}
}
32 changes: 32 additions & 0 deletions src/__tests__/getPaginationMeta.test.ts
@@ -0,0 +1,32 @@
import { getPaginationMeta, PaginationState } from "../getPaginationMeta";

const MULTI_PAGE_FIRST_PAGE: PaginationState = {
totalItems: 100,
pageSize: 10,
currentPage: 0,
};

describe("getPaginationMeta", () => {
it("correctly calculates startIndex and lastIndex on the first page", () => {
const meta = getPaginationMeta(MULTI_PAGE_FIRST_PAGE);
expect(meta.startIndex).toBe(0);
expect(meta.endIndex).toBe(9);
});

it("correctly calculates startIndex and endIndex on the second page", () => {
const meta = getPaginationMeta({ ...MULTI_PAGE_FIRST_PAGE, currentPage: 1 });
expect(meta.startIndex).toBe(10);
expect(meta.endIndex).toBe(19);
});

it("correctly calculates startIndex and endIndex on the last page", () => {
const meta = getPaginationMeta({ ...MULTI_PAGE_FIRST_PAGE, currentPage: 9 });
expect(meta.startIndex).toBe(90);
expect(meta.endIndex).toBe(99);
});

it("correctly calculates endIndex on a half-full last page", () => {
const meta = getPaginationMeta({ totalItems: 92, pageSize: 10, currentPage: 9 });
expect(meta.endIndex).toBe(91);
});
});
49 changes: 0 additions & 49 deletions src/__tests__/getPaginationState.test.ts

This file was deleted.

140 changes: 140 additions & 0 deletions src/__tests__/paginationStateReducer.test.ts
@@ -0,0 +1,140 @@
import { paginationStateReducer } from "../paginationStateReducer";
import { PaginationState } from "../getPaginationMeta";

const MULTI_PAGE_FIRST_PAGE: PaginationState = {
totalItems: 100,
pageSize: 10,
currentPage: 0,
};

describe("paginationStateReducer", () => {
it("sets the next page when not on the last page", () => {
const nextState = paginationStateReducer(MULTI_PAGE_FIRST_PAGE, { type: "NEXT_PAGE" });
expect(nextState.currentPage).toBe(1);
});

it("does not set the next page when on the last page", () => {
const nextState = paginationStateReducer(
{ totalItems: 1, pageSize: 1, currentPage: 0 },
{ type: "NEXT_PAGE" }
);
expect(nextState.currentPage).toBe(0);
});

it("sets the previous page when not on the first page", () => {
const nextState = paginationStateReducer(
{ totalItems: 2, pageSize: 1, currentPage: 1 },
{ type: "PREVIOUS_PAGE" }
);
expect(nextState.currentPage).toBe(0);
});

it("does not set the previous page when on the first page", () => {
const nextState = paginationStateReducer(MULTI_PAGE_FIRST_PAGE, { type: "PREVIOUS_PAGE" });
expect(nextState.currentPage).toBe(0);
});

it("allows totalPages to be set", () => {
const nextTotalItems = 12;
const nextState = paginationStateReducer(MULTI_PAGE_FIRST_PAGE, {
type: "SET_TOTALITEMS",
totalItems: nextTotalItems,
});

expect(nextState.totalItems).toBe(nextTotalItems);
});

it("allows pageSize to be set", () => {
const nextPageSize = 12;
const nextState = paginationStateReducer(MULTI_PAGE_FIRST_PAGE, {
type: "SET_PAGESIZE",
pageSize: nextPageSize,
});

expect(nextState.pageSize).toBe(nextPageSize);
});

it("allows currentPage to be set", () => {
const nextCurrentPage = 12;
const nextState = paginationStateReducer(
{ totalItems: 100, pageSize: 1, currentPage: 0 },
{
type: "SET_PAGE",
page: nextCurrentPage,
}
);

expect(nextState.currentPage).toBe(nextCurrentPage);
});

it("disallows currentPage from being set below 0", () => {
const nextCurrentPage = -1;
const nextState = paginationStateReducer(MULTI_PAGE_FIRST_PAGE, {
type: "SET_PAGE",
page: nextCurrentPage,
});

expect(nextState.currentPage).toBe(0);
});

it("disallows currentPage from being set above totalPages", () => {
const nextCurrentPage = 1;
const nextState = paginationStateReducer(
{ totalItems: 1, pageSize: 1, currentPage: 0 },
{
type: "SET_PAGE",
page: nextCurrentPage,
}
);

expect(nextState.currentPage).toBe(0);
});

it("limits currentPage within totalPages when pageSize is increased", () => {
const nextState = paginationStateReducer(
{ totalItems: 100, pageSize: 10, currentPage: 9 },
{
type: "SET_PAGESIZE",
pageSize: 50,
}
);

expect(nextState.currentPage).toBe(1);
});

it("doesn't change currentPage if it wouldn't be out of bounds when pageSize is increased", () => {
const nextState = paginationStateReducer(
{ totalItems: 100, pageSize: 10, currentPage: 2 },
{
type: "SET_PAGESIZE",
pageSize: 25,
}
);

expect(nextState.currentPage).toBe(2);
});

it("limits currentPage within totalPages when totalItems is decreased", () => {
const nextState = paginationStateReducer(
{ totalItems: 100, pageSize: 10, currentPage: 9 },
{
type: "SET_TOTALITEMS",
totalItems: 20,
}
);

expect(nextState.currentPage).toBe(1);
});

it("doesn't change currentPage if it wouldn't be out of bounds when totalItems is decreased", () => {
const nextState = paginationStateReducer(
{ totalItems: 100, pageSize: 10, currentPage: 9 },
{
type: "SET_TOTALITEMS",
totalItems: 95,
}
);

expect(nextState.currentPage).toBe(9);
});
});
79 changes: 79 additions & 0 deletions src/__tests__/usePagination.test.ts
@@ -0,0 +1,79 @@
import { renderHook, act } from "@testing-library/react-hooks";
import { usePagination } from "../usePagination";

const DEFAULT_STATE = { totalItems: 100, initialPageSize: 10, initialPage: 1 };

describe("usePagination", () => {
it("correctly initializes the page state and contains the expected metadata", () => {
const { result } = renderHook(() => usePagination(DEFAULT_STATE));

expect(result.current.currentPage).toBe(DEFAULT_STATE.initialPage);
expect(result.current.totalItems).toBe(DEFAULT_STATE.totalItems);
expect(result.current.pageSize).toBe(DEFAULT_STATE.initialPageSize);
expect(result.current.startIndex).toBe(
DEFAULT_STATE.initialPage * DEFAULT_STATE.initialPageSize
);
expect(result.current.endIndex).toBe(
(DEFAULT_STATE.initialPage + 1) * DEFAULT_STATE.initialPageSize - 1
);
expect(result.current.nextEnabled).toBe(true);
expect(result.current.previousEnabled).toBe(true);
});

it("sets the next page when setNextPage is called", () => {
const { result } = renderHook(() => usePagination(DEFAULT_STATE));

expect(result.current.currentPage).toBe(DEFAULT_STATE.initialPage);

act(() => {
result.current.setNextPage();
});

expect(result.current.currentPage).toBe(DEFAULT_STATE.initialPage + 1);
});

it("sets the previous page when setPreviousPage is called", () => {
const { result } = renderHook(() => usePagination(DEFAULT_STATE));

expect(result.current.currentPage).toBe(DEFAULT_STATE.initialPage);

act(() => {
result.current.setPreviousPage();
});

expect(result.current.currentPage).toBe(DEFAULT_STATE.initialPage - 1);
});

it("sets the page when setPage is called", () => {
const { result } = renderHook(() => usePagination(DEFAULT_STATE));

expect(result.current.currentPage).toBe(DEFAULT_STATE.initialPage);

act(() => {
result.current.setPage(0);
});

expect(result.current.currentPage).toBe(0);
});

it("sets the pageSize when setPageSize is called", () => {
const { result } = renderHook(() => usePagination(DEFAULT_STATE));

expect(result.current.pageSize).toBe(DEFAULT_STATE.initialPageSize);

act(() => {
result.current.setPageSize(5);
});

expect(result.current.pageSize).toBe(5);
});

// This is required so that the hook can be rendered before server-provided data is available
it("initializes configurations to 0 when not provided", () => {
const { result } = renderHook(() => usePagination());

expect(result.current.totalItems).toBe(0);
expect(result.current.pageSize).toBe(0);
expect(result.current.currentPage).toBe(0);
});
});
19 changes: 13 additions & 6 deletions src/getPaginationState.ts → src/getPaginationMeta.ts
Expand Up @@ -19,23 +19,30 @@ export const getEndIndex = (pageSize: number, currentPage: number, totalItems: n
return lastPageEndIndex - 1;
};

export const limitPageBounds =
(totalItems: number, pageSize: number) =>
(page: number): number =>
Math.min(Math.max(page, 0), getTotalPages(totalItems, pageSize) - 1);

export type PaginationState = {
totalItems: number;
pageSize: number;
currentPage: number;
};

export type PaginationMeta = {
totalPages: number;
startIndex: number;
endIndex: number;
previousEnabled: boolean;
nextEnabled: boolean;
};

export const getPaginationState = ({
export const getPaginationMeta = ({
totalItems,
pageSize,
currentPage,
}: {
totalItems: number;
pageSize: number;
currentPage: number;
}): PaginationState => {
}: PaginationState): PaginationMeta => {
const totalPages = getTotalPages(totalItems, pageSize);
return {
totalPages,
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
@@ -1,3 +1,3 @@
export * from "./getPaginationState";
export * from "./getPaginationMeta";
export * from "./Pagination";
export * from "./usePagination";

0 comments on commit cfdd6ec

Please sign in to comment.