@boxcustodia/library 2.0.0-alpha.22 → 2.0.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/calendar/calendar.cjs.js +1 -1
- package/dist/components/calendar/calendar.es.js +43 -44
- package/dist/components/date-picker/date-input.cjs.js +1 -1
- package/dist/components/date-picker/date-input.es.js +160 -140
- package/dist/components/pagination/pagination.cjs.js +1 -1
- package/dist/components/pagination/pagination.es.js +37 -35
- package/dist/components/scroll-area/scroll-area.cjs.js +1 -1
- package/dist/components/scroll-area/scroll-area.es.js +4 -4
- package/dist/components/select/select.cjs.js +1 -1
- package/dist/components/select/select.es.js +94 -90
- package/dist/hooks/use-action/use-action.cjs.js +1 -0
- package/dist/hooks/use-action/use-action.es.js +41 -0
- package/dist/hooks/use-pagination/use-pagination.cjs.js +1 -1
- package/dist/hooks/use-pagination/use-pagination.es.js +77 -32
- package/dist/hooks/use-range-pagination/use-range-pagination.cjs.js +1 -1
- package/dist/hooks/use-range-pagination/use-range-pagination.es.js +8 -5
- package/dist/hooks/use-selection/use-selection.cjs.js +1 -1
- package/dist/hooks/use-selection/use-selection.es.js +95 -33
- package/dist/hooks/use-session-storage/use-session-storage.cjs.js +1 -0
- package/dist/hooks/use-session-storage/use-session-storage.es.js +57 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +61 -63
- package/dist/src/components/select/select.d.ts +9 -2
- package/dist/src/hooks/index.d.ts +2 -3
- package/dist/src/hooks/internal/index.d.ts +1 -0
- package/dist/src/hooks/internal/serializer.d.ts +4 -0
- package/dist/src/hooks/use-action/index.d.ts +1 -0
- package/dist/src/hooks/use-action/use-action.d.ts +22 -0
- package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +2 -4
- package/dist/src/hooks/use-pagination/use-pagination.d.ts +47 -32
- package/dist/src/hooks/use-range-pagination/use-range-pagination.d.ts +16 -10
- package/dist/src/hooks/use-selection/use-selection.d.ts +39 -45
- package/dist/src/hooks/use-session-storage/index.d.ts +1 -0
- package/dist/src/hooks/use-session-storage/use-session-storage.d.ts +11 -0
- package/package.json +1 -1
- package/src/components/calendar/calendar.tsx +10 -8
- package/src/components/combobox/combobox.stories.tsx +16 -0
- package/src/components/date-picker/date-input.tsx +23 -2
- package/src/components/form/form.tsx +3 -2
- package/src/components/pagination/pagination.tsx +5 -3
- package/src/components/scroll-area/scroll-area.tsx +2 -2
- package/src/components/select/select.tsx +14 -3
- package/src/hooks/index.ts +2 -3
- package/src/hooks/internal/index.ts +1 -0
- package/src/hooks/internal/serializer.ts +4 -0
- package/src/hooks/use-action/index.ts +1 -0
- package/src/hooks/{use-mutation/use-mutation.stories.tsx → use-action/use-action.stories.tsx} +34 -34
- package/src/hooks/{use-mutation/use-mutation.test.ts → use-action/use-action.test.ts} +53 -53
- package/src/hooks/{use-mutation/use-mutation.ts → use-action/use-action.ts} +20 -20
- package/src/hooks/use-click-outside/use-click-outside.stories.tsx +0 -1
- package/src/hooks/use-clipboard/use-clipboard.stories.tsx +0 -1
- package/src/hooks/use-document-title/use-document-title.stories.tsx +0 -1
- package/src/hooks/use-is-visible/use-is-visible.test.tsx +1 -1
- package/src/hooks/use-local-storage/use-local-storage.stories.tsx +0 -1
- package/src/hooks/use-local-storage/use-local-storage.ts +2 -5
- package/src/hooks/use-media-query/use-media-query.stories.tsx +0 -1
- package/src/hooks/use-pagination/use-pagination.stories.tsx +720 -57
- package/src/hooks/use-pagination/use-pagination.test.tsx +560 -48
- package/src/hooks/use-pagination/use-pagination.ts +266 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +0 -1
- package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +2 -2
- package/src/hooks/use-range-pagination/use-range-pagination.tsx +24 -21
- package/src/hooks/use-selection/use-selection.stories.tsx +339 -84
- package/src/hooks/use-selection/use-selection.test.tsx +417 -2
- package/src/hooks/use-selection/use-selection.ts +212 -102
- package/src/hooks/use-session-storage/index.ts +1 -0
- package/src/hooks/use-session-storage/use-session-storage.stories.tsx +122 -0
- package/src/hooks/use-session-storage/use-session-storage.test.ts +164 -0
- package/src/hooks/use-session-storage/use-session-storage.ts +115 -0
- package/dist/hooks/use-async/use-async.cjs.js +0 -1
- package/dist/hooks/use-async/use-async.es.js +0 -57
- package/dist/hooks/use-focus-trap/scope-tab.cjs.js +0 -1
- package/dist/hooks/use-focus-trap/scope-tab.es.js +0 -21
- package/dist/hooks/use-focus-trap/tabbable.cjs.js +0 -1
- package/dist/hooks/use-focus-trap/tabbable.es.js +0 -38
- package/dist/hooks/use-focus-trap/use-focus-trap.cjs.js +0 -1
- package/dist/hooks/use-focus-trap/use-focus-trap.es.js +0 -34
- package/dist/hooks/use-mutation/use-mutation.cjs.js +0 -1
- package/dist/hooks/use-mutation/use-mutation.es.js +0 -41
- package/dist/src/hooks/use-async/index.d.ts +0 -1
- package/dist/src/hooks/use-async/use-async.d.ts +0 -21
- package/dist/src/hooks/use-focus-trap/index.d.ts +0 -1
- package/dist/src/hooks/use-focus-trap/scope-tab.d.ts +0 -1
- package/dist/src/hooks/use-focus-trap/tabbable.d.ts +0 -4
- package/dist/src/hooks/use-focus-trap/use-focus-trap.d.ts +0 -1
- package/dist/src/hooks/use-mutation/index.d.ts +0 -1
- package/dist/src/hooks/use-mutation/use-mutation.d.ts +0 -22
- package/dist/src/hooks/use-mutation/use-mutation.test.d.ts +0 -1
- package/src/hooks/use-async/index.ts +0 -1
- package/src/hooks/use-async/use-async.stories.tsx +0 -272
- package/src/hooks/use-async/use-async.test.ts +0 -397
- package/src/hooks/use-async/use-async.ts +0 -135
- package/src/hooks/use-focus-trap/index.ts +0 -1
- package/src/hooks/use-focus-trap/scope-tab.ts +0 -38
- package/src/hooks/use-focus-trap/tabbable.ts +0 -70
- package/src/hooks/use-focus-trap/use-focus-trap.stories.tsx +0 -37
- package/src/hooks/use-focus-trap/use-focus-trap.test.ts +0 -355
- package/src/hooks/use-focus-trap/use-focus-trap.ts +0 -78
- package/src/hooks/use-mutation/index.ts +0 -1
- package/src/hooks/use-pagination/use-pagination.tsx +0 -84
- /package/dist/src/hooks/{use-async/use-async.test.d.ts → use-action/use-action.test.d.ts} +0 -0
- /package/dist/src/hooks/{use-focus-trap/use-focus-trap.test.d.ts → use-session-storage/use-session-storage.test.d.ts} +0 -0
|
@@ -1,98 +1,610 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
act,
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
renderHook,
|
|
6
|
+
screen,
|
|
7
|
+
} from "@testing-library/react";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
10
|
import { usePagination } from "../use-pagination";
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Scenarios 1–5: Default Initial State
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
describe("usePagination — Default Initial State", () => {
|
|
17
|
+
it("Scenario 1 — default initialization: page=1, pageSize=10, pageCount=10", () => {
|
|
18
|
+
const { result } = renderHook(() => usePagination({ totalItems: 100 }));
|
|
19
|
+
expect(result.current.page).toBe(1);
|
|
20
|
+
expect(result.current.pageSize).toBe(10);
|
|
21
|
+
expect(result.current.pageCount).toBe(10);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("Scenario 2 — defaultPage seeds initial page", () => {
|
|
25
|
+
const { result } = renderHook(() =>
|
|
26
|
+
usePagination({ totalItems: 100, defaultPage: 3, defaultPageSize: 10 }),
|
|
27
|
+
);
|
|
28
|
+
expect(result.current.page).toBe(3);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("Scenario 3 — defaultPageSize seeds initial pageSize", () => {
|
|
32
|
+
const { result } = renderHook(() =>
|
|
33
|
+
usePagination({ totalItems: 100, defaultPageSize: 20 }),
|
|
34
|
+
);
|
|
35
|
+
expect(result.current.pageSize).toBe(20);
|
|
36
|
+
expect(result.current.pageCount).toBe(5);
|
|
8
37
|
});
|
|
9
38
|
|
|
10
|
-
it("
|
|
39
|
+
it("Scenario 4 — totalItems=0 edge case", () => {
|
|
40
|
+
const { result } = renderHook(() => usePagination({ totalItems: 0 }));
|
|
41
|
+
expect(result.current.pageCount).toBe(0);
|
|
42
|
+
expect(result.current.page).toBe(1);
|
|
43
|
+
expect(result.current.isFirstPage).toBe(true);
|
|
44
|
+
expect(result.current.isLastPage).toBe(true);
|
|
45
|
+
expect(result.current.hasPrevPage).toBe(false);
|
|
46
|
+
expect(result.current.hasNextPage).toBe(false);
|
|
47
|
+
expect(result.current.range).toEqual({ start: 0, end: 0 });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("Scenario 5 — pageSize=0 guard: coerced to 1, console.warn called", () => {
|
|
51
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
52
|
+
const { result } = renderHook(() =>
|
|
53
|
+
usePagination({ totalItems: 100, defaultPageSize: 0 }),
|
|
54
|
+
);
|
|
55
|
+
expect(result.current.pageSize).toBe(1);
|
|
56
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
57
|
+
warnSpy.mockRestore();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Scenarios 6–16: Navigation Actions
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
describe("usePagination — Navigation Actions", () => {
|
|
66
|
+
it("Scenario 6 — next() increments page", () => {
|
|
67
|
+
const onPageChange = vi.fn();
|
|
11
68
|
const { result } = renderHook(() =>
|
|
12
69
|
usePagination({
|
|
13
|
-
totalItems:
|
|
14
|
-
|
|
70
|
+
totalItems: 50,
|
|
71
|
+
defaultPageSize: 10,
|
|
72
|
+
defaultPage: 2,
|
|
73
|
+
onPageChange,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
act(() => result.current.next());
|
|
77
|
+
expect(result.current.page).toBe(3);
|
|
78
|
+
expect(onPageChange).toHaveBeenCalledWith(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("Scenario 7 — next() no-op at last page", () => {
|
|
82
|
+
const onPageChange = vi.fn();
|
|
83
|
+
const { result } = renderHook(() =>
|
|
84
|
+
usePagination({
|
|
85
|
+
totalItems: 50,
|
|
86
|
+
defaultPageSize: 10,
|
|
87
|
+
defaultPage: 5,
|
|
88
|
+
onPageChange,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
act(() => result.current.next());
|
|
92
|
+
expect(result.current.page).toBe(5);
|
|
93
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("Scenario 8 — prev() decrements page", () => {
|
|
97
|
+
const onPageChange = vi.fn();
|
|
98
|
+
const { result } = renderHook(() =>
|
|
99
|
+
usePagination({
|
|
100
|
+
totalItems: 50,
|
|
101
|
+
defaultPageSize: 10,
|
|
102
|
+
defaultPage: 3,
|
|
103
|
+
onPageChange,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
act(() => result.current.prev());
|
|
107
|
+
expect(result.current.page).toBe(2);
|
|
108
|
+
expect(onPageChange).toHaveBeenCalledWith(2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("Scenario 9 — prev() no-op at first page", () => {
|
|
112
|
+
const onPageChange = vi.fn();
|
|
113
|
+
const { result } = renderHook(() =>
|
|
114
|
+
usePagination({
|
|
115
|
+
totalItems: 50,
|
|
116
|
+
defaultPageSize: 10,
|
|
117
|
+
defaultPage: 1,
|
|
118
|
+
onPageChange,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
act(() => result.current.prev());
|
|
122
|
+
expect(result.current.page).toBe(1);
|
|
123
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("Scenario 10 — firstPage() jumps to page 1", () => {
|
|
127
|
+
const onPageChange = vi.fn();
|
|
128
|
+
const { result } = renderHook(() =>
|
|
129
|
+
usePagination({
|
|
130
|
+
totalItems: 50,
|
|
131
|
+
defaultPageSize: 10,
|
|
132
|
+
defaultPage: 4,
|
|
133
|
+
onPageChange,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
act(() => result.current.firstPage());
|
|
137
|
+
expect(result.current.page).toBe(1);
|
|
138
|
+
expect(onPageChange).toHaveBeenCalledWith(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("Scenario 10b — firstPage() no-op when already first", () => {
|
|
142
|
+
const onPageChange = vi.fn();
|
|
143
|
+
const { result } = renderHook(() =>
|
|
144
|
+
usePagination({
|
|
145
|
+
totalItems: 50,
|
|
146
|
+
defaultPageSize: 10,
|
|
147
|
+
defaultPage: 1,
|
|
148
|
+
onPageChange,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
act(() => result.current.firstPage());
|
|
152
|
+
expect(result.current.page).toBe(1);
|
|
153
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("Scenario 11 — lastPage() jumps to pageCount", () => {
|
|
157
|
+
const onPageChange = vi.fn();
|
|
158
|
+
const { result } = renderHook(() =>
|
|
159
|
+
usePagination({
|
|
160
|
+
totalItems: 50,
|
|
161
|
+
defaultPageSize: 10,
|
|
162
|
+
defaultPage: 2,
|
|
163
|
+
onPageChange,
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
act(() => result.current.lastPage());
|
|
167
|
+
expect(result.current.page).toBe(5);
|
|
168
|
+
expect(onPageChange).toHaveBeenCalledWith(5);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("Scenario 11b — lastPage() no-op when already last", () => {
|
|
172
|
+
const onPageChange = vi.fn();
|
|
173
|
+
const { result } = renderHook(() =>
|
|
174
|
+
usePagination({
|
|
175
|
+
totalItems: 50,
|
|
176
|
+
defaultPageSize: 10,
|
|
177
|
+
defaultPage: 5,
|
|
178
|
+
onPageChange,
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
act(() => result.current.lastPage());
|
|
182
|
+
expect(result.current.page).toBe(5);
|
|
183
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("Scenario 12 — goTo(n) jumps to n", () => {
|
|
187
|
+
const onPageChange = vi.fn();
|
|
188
|
+
const { result } = renderHook(() =>
|
|
189
|
+
usePagination({
|
|
190
|
+
totalItems: 50,
|
|
191
|
+
defaultPageSize: 10,
|
|
192
|
+
defaultPage: 1,
|
|
193
|
+
onPageChange,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
act(() => result.current.goTo(3));
|
|
197
|
+
expect(result.current.page).toBe(3);
|
|
198
|
+
expect(onPageChange).toHaveBeenCalledWith(3);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("Scenario 13 — goTo clamps above pageCount", () => {
|
|
202
|
+
const onPageChange = vi.fn();
|
|
203
|
+
const { result } = renderHook(() =>
|
|
204
|
+
usePagination({
|
|
205
|
+
totalItems: 50,
|
|
206
|
+
defaultPageSize: 10,
|
|
207
|
+
defaultPage: 1,
|
|
208
|
+
onPageChange,
|
|
15
209
|
}),
|
|
16
210
|
);
|
|
17
|
-
|
|
211
|
+
act(() => result.current.goTo(99));
|
|
212
|
+
expect(result.current.page).toBe(5);
|
|
213
|
+
expect(onPageChange).toHaveBeenCalledWith(5);
|
|
214
|
+
});
|
|
18
215
|
|
|
19
|
-
|
|
20
|
-
|
|
216
|
+
it("Scenario 14 — goTo clamps below 1", () => {
|
|
217
|
+
const onPageChange = vi.fn();
|
|
218
|
+
const { result } = renderHook(() =>
|
|
219
|
+
usePagination({ totalItems: 50, defaultPageSize: 10, onPageChange }),
|
|
220
|
+
);
|
|
221
|
+
act(() => result.current.goTo(-2));
|
|
222
|
+
expect(result.current.page).toBe(1);
|
|
223
|
+
// Already at page 1, no change → no-op
|
|
224
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
21
225
|
});
|
|
22
226
|
|
|
23
|
-
it("
|
|
227
|
+
it("Scenario 15 — goTo(currentPage) is no-op", () => {
|
|
228
|
+
const onPageChange = vi.fn();
|
|
229
|
+
const { result } = renderHook(() =>
|
|
230
|
+
usePagination({
|
|
231
|
+
totalItems: 50,
|
|
232
|
+
defaultPageSize: 10,
|
|
233
|
+
defaultPage: 3,
|
|
234
|
+
onPageChange,
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
act(() => result.current.goTo(3));
|
|
238
|
+
expect(result.current.page).toBe(3);
|
|
239
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("Scenario 16 — goTo when pageCount=0 is no-op", () => {
|
|
243
|
+
const onPageChange = vi.fn();
|
|
244
|
+
const { result } = renderHook(() =>
|
|
245
|
+
usePagination({ totalItems: 0, onPageChange }),
|
|
246
|
+
);
|
|
247
|
+
act(() => result.current.goTo(1));
|
|
248
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Scenarios 17–19: Page Size Control
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
describe("usePagination — Page Size Control", () => {
|
|
257
|
+
it("Scenario 17 — setPageSize(n) changes pageSize and resets page to 1", () => {
|
|
258
|
+
const { result } = renderHook(() =>
|
|
259
|
+
usePagination({ totalItems: 100, defaultPageSize: 10, defaultPage: 3 }),
|
|
260
|
+
);
|
|
261
|
+
act(() => result.current.setPageSize(20));
|
|
262
|
+
expect(result.current.pageSize).toBe(20);
|
|
263
|
+
expect(result.current.page).toBe(1);
|
|
264
|
+
expect(result.current.pageCount).toBe(5);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("Scenario 18 — setPageSize fires callbacks in order: onPageSizeChange then onPageChange(1)", () => {
|
|
268
|
+
const onPageSizeChange = vi.fn();
|
|
269
|
+
const onPageChange = vi.fn();
|
|
24
270
|
const { result } = renderHook(() =>
|
|
25
271
|
usePagination({
|
|
26
272
|
totalItems: 100,
|
|
27
|
-
|
|
28
|
-
|
|
273
|
+
defaultPageSize: 10,
|
|
274
|
+
defaultPage: 3,
|
|
275
|
+
onPageSizeChange,
|
|
276
|
+
onPageChange,
|
|
29
277
|
}),
|
|
30
278
|
);
|
|
31
|
-
|
|
32
|
-
expect(
|
|
279
|
+
act(() => result.current.setPageSize(20));
|
|
280
|
+
expect(onPageSizeChange).toHaveBeenCalledWith(20);
|
|
281
|
+
expect(onPageChange).toHaveBeenCalledWith(1);
|
|
33
282
|
});
|
|
34
283
|
|
|
35
|
-
it("
|
|
284
|
+
it("Scenario 19 — setPageSize same size is no-op", () => {
|
|
285
|
+
const onPageSizeChange = vi.fn();
|
|
286
|
+
const onPageChange = vi.fn();
|
|
36
287
|
const { result } = renderHook(() =>
|
|
37
288
|
usePagination({
|
|
38
289
|
totalItems: 100,
|
|
39
|
-
|
|
290
|
+
defaultPageSize: 10,
|
|
291
|
+
onPageSizeChange,
|
|
292
|
+
onPageChange,
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
act(() => result.current.setPageSize(10));
|
|
296
|
+
expect(onPageSizeChange).not.toHaveBeenCalled();
|
|
297
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Scenarios 20–23: Controllable Page
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
describe("usePagination — Controllable Page", () => {
|
|
306
|
+
it("Scenario 20 — page prop controls current page", () => {
|
|
307
|
+
const { result } = renderHook(() =>
|
|
308
|
+
usePagination({ totalItems: 50, defaultPageSize: 10, page: 3 }),
|
|
309
|
+
);
|
|
310
|
+
expect(result.current.page).toBe(3);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("Scenario 21 — onPageChange fires on navigation (controlled)", () => {
|
|
314
|
+
const onPageChange = vi.fn();
|
|
315
|
+
const { result } = renderHook(() =>
|
|
316
|
+
usePagination({
|
|
317
|
+
totalItems: 50,
|
|
318
|
+
defaultPageSize: 10,
|
|
319
|
+
page: 2,
|
|
320
|
+
onPageChange,
|
|
40
321
|
}),
|
|
41
322
|
);
|
|
42
|
-
|
|
43
|
-
|
|
323
|
+
act(() => result.current.next());
|
|
324
|
+
expect(onPageChange).toHaveBeenCalledWith(3);
|
|
325
|
+
});
|
|
44
326
|
|
|
45
|
-
|
|
46
|
-
|
|
327
|
+
it("Scenario 22 — onPageChange NOT fired on mount", () => {
|
|
328
|
+
const onPageChange = vi.fn();
|
|
329
|
+
renderHook(() =>
|
|
330
|
+
usePagination({
|
|
331
|
+
totalItems: 50,
|
|
332
|
+
defaultPageSize: 10,
|
|
333
|
+
page: 1,
|
|
334
|
+
onPageChange,
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
47
338
|
});
|
|
48
339
|
|
|
49
|
-
it("
|
|
340
|
+
it("Scenario 23 — onPageChange NOT fired for no-op actions", () => {
|
|
341
|
+
const onPageChange = vi.fn();
|
|
50
342
|
const { result } = renderHook(() =>
|
|
51
343
|
usePagination({
|
|
52
|
-
totalItems:
|
|
53
|
-
|
|
54
|
-
|
|
344
|
+
totalItems: 50,
|
|
345
|
+
defaultPageSize: 10,
|
|
346
|
+
page: 1,
|
|
347
|
+
onPageChange,
|
|
55
348
|
}),
|
|
56
349
|
);
|
|
57
|
-
|
|
58
|
-
|
|
350
|
+
act(() => result.current.prev());
|
|
351
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Scenarios 24–26: Controllable Page Size
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
describe("usePagination — Controllable Page Size", () => {
|
|
360
|
+
it("Scenario 24 — pageSize prop controls current pageSize", () => {
|
|
361
|
+
const { result } = renderHook(() =>
|
|
362
|
+
usePagination({ totalItems: 100, pageSize: 20 }),
|
|
363
|
+
);
|
|
364
|
+
expect(result.current.pageSize).toBe(20);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("Scenario 25 — onPageSizeChange fires after setPageSize (controlled)", () => {
|
|
368
|
+
const onPageSizeChange = vi.fn();
|
|
369
|
+
const { result } = renderHook(() =>
|
|
370
|
+
usePagination({ totalItems: 100, pageSize: 10, onPageSizeChange }),
|
|
371
|
+
);
|
|
372
|
+
act(() => result.current.setPageSize(25));
|
|
373
|
+
expect(onPageSizeChange).toHaveBeenCalledWith(25);
|
|
374
|
+
});
|
|
59
375
|
|
|
60
|
-
|
|
61
|
-
|
|
376
|
+
it("Scenario 26 — onPageSizeChange NOT fired on mount", () => {
|
|
377
|
+
const onPageSizeChange = vi.fn();
|
|
378
|
+
renderHook(() =>
|
|
379
|
+
usePagination({ totalItems: 100, pageSize: 10, onPageSizeChange }),
|
|
380
|
+
);
|
|
381
|
+
expect(onPageSizeChange).not.toHaveBeenCalled();
|
|
62
382
|
});
|
|
63
383
|
|
|
64
|
-
it("
|
|
384
|
+
it("Scenario 26b — setPageSize fires onPageChange(1) in controlled mode when page prop > 1", () => {
|
|
385
|
+
const onPageChange = vi.fn();
|
|
386
|
+
const { result } = renderHook(() =>
|
|
387
|
+
usePagination({ totalItems: 100, page: 3, onPageChange }),
|
|
388
|
+
);
|
|
389
|
+
act(() => result.current.setPageSize(20));
|
|
390
|
+
expect(onPageChange).toHaveBeenCalledWith(1);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("Scenario 26c — fully controlled: setPageSize batches page+pageSize so a consumer effect keyed on [page, pageSize] runs once", () => {
|
|
394
|
+
const fetchSpy = vi.fn();
|
|
395
|
+
|
|
396
|
+
function Consumer() {
|
|
397
|
+
const [page, setPage] = useState(3);
|
|
398
|
+
const [pageSize, setPageSize] = useState(10);
|
|
399
|
+
const pagination = usePagination({
|
|
400
|
+
totalItems: 100,
|
|
401
|
+
page,
|
|
402
|
+
onPageChange: setPage,
|
|
403
|
+
pageSize,
|
|
404
|
+
onPageSizeChange: setPageSize,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
fetchSpy({ page: pagination.page, pageSize: pagination.pageSize });
|
|
409
|
+
}, [pagination.page, pagination.pageSize]);
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<button type="button" onClick={() => pagination.setPageSize(20)}>
|
|
413
|
+
change
|
|
414
|
+
</button>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
render(<Consumer />);
|
|
419
|
+
// Ignore the mount fire — we only care about the transition.
|
|
420
|
+
fetchSpy.mockClear();
|
|
421
|
+
|
|
422
|
+
fireEvent.click(screen.getByText("change"));
|
|
423
|
+
|
|
424
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
425
|
+
expect(fetchSpy).toHaveBeenCalledWith({ page: 1, pageSize: 20 });
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
// Scenarios 26d–26g: Unified onPaginationChange
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
describe("usePagination — Unified onPaginationChange", () => {
|
|
434
|
+
it("Scenario 26d — NOT fired on mount", () => {
|
|
435
|
+
const onPaginationChange = vi.fn();
|
|
436
|
+
renderHook(() => usePagination({ totalItems: 100, onPaginationChange }));
|
|
437
|
+
expect(onPaginationChange).not.toHaveBeenCalled();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("Scenario 26e — fires once with both values when navigating", () => {
|
|
441
|
+
const onPaginationChange = vi.fn();
|
|
65
442
|
const { result } = renderHook(() =>
|
|
66
443
|
usePagination({
|
|
67
444
|
totalItems: 100,
|
|
68
|
-
|
|
445
|
+
defaultPageSize: 10,
|
|
446
|
+
onPaginationChange,
|
|
69
447
|
}),
|
|
70
448
|
);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
waitFor(() => {
|
|
75
|
-
expect(currentPage).toBe(5);
|
|
76
|
-
});
|
|
449
|
+
act(() => result.current.next());
|
|
450
|
+
expect(onPaginationChange).toHaveBeenCalledTimes(1);
|
|
451
|
+
expect(onPaginationChange).toHaveBeenCalledWith({ page: 2, pageSize: 10 });
|
|
77
452
|
});
|
|
78
453
|
|
|
79
|
-
it("
|
|
454
|
+
it("Scenario 26f — fires once with both final values when setPageSize resets page", () => {
|
|
455
|
+
const onPaginationChange = vi.fn();
|
|
80
456
|
const { result } = renderHook(() =>
|
|
81
457
|
usePagination({
|
|
82
458
|
totalItems: 100,
|
|
83
|
-
|
|
84
|
-
|
|
459
|
+
defaultPage: 3,
|
|
460
|
+
defaultPageSize: 10,
|
|
461
|
+
onPaginationChange,
|
|
85
462
|
}),
|
|
86
463
|
);
|
|
464
|
+
act(() => result.current.setPageSize(20));
|
|
465
|
+
expect(onPaginationChange).toHaveBeenCalledTimes(1);
|
|
466
|
+
expect(onPaginationChange).toHaveBeenCalledWith({ page: 1, pageSize: 20 });
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("Scenario 26g — fully controlled: setPageSize triggers a single onPaginationChange with both values", () => {
|
|
470
|
+
const changeSpy = vi.fn();
|
|
471
|
+
|
|
472
|
+
function Consumer() {
|
|
473
|
+
const [page, setPage] = useState(3);
|
|
474
|
+
const [pageSize, setPageSize] = useState(10);
|
|
475
|
+
const pagination = usePagination({
|
|
476
|
+
totalItems: 100,
|
|
477
|
+
page,
|
|
478
|
+
onPageChange: setPage,
|
|
479
|
+
pageSize,
|
|
480
|
+
onPageSizeChange: setPageSize,
|
|
481
|
+
onPaginationChange: changeSpy,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
<button type="button" onClick={() => pagination.setPageSize(20)}>
|
|
486
|
+
change
|
|
487
|
+
</button>
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
render(<Consumer />);
|
|
492
|
+
changeSpy.mockClear();
|
|
493
|
+
|
|
494
|
+
fireEvent.click(screen.getByText("change"));
|
|
495
|
+
|
|
496
|
+
expect(changeSpy).toHaveBeenCalledTimes(1);
|
|
497
|
+
expect(changeSpy).toHaveBeenCalledWith({ page: 1, pageSize: 20 });
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// Scenarios 27–33: Derived State
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
describe("usePagination — Derived State", () => {
|
|
506
|
+
it("Scenario 27 — isFirstPage=true when page=1", () => {
|
|
507
|
+
const { result } = renderHook(() =>
|
|
508
|
+
usePagination({ totalItems: 50, defaultPageSize: 10, defaultPage: 1 }),
|
|
509
|
+
);
|
|
510
|
+
expect(result.current.isFirstPage).toBe(true);
|
|
511
|
+
expect(result.current.hasPrevPage).toBe(false);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("Scenario 28 — isLastPage=true when page=pageCount", () => {
|
|
515
|
+
const { result } = renderHook(() =>
|
|
516
|
+
usePagination({ totalItems: 50, defaultPageSize: 10, defaultPage: 5 }),
|
|
517
|
+
);
|
|
518
|
+
expect(result.current.isLastPage).toBe(true);
|
|
519
|
+
expect(result.current.hasNextPage).toBe(false);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("Scenario 29 — hasPrevPage is inverse of isFirstPage", () => {
|
|
523
|
+
const { result } = renderHook(() =>
|
|
524
|
+
usePagination({ totalItems: 50, defaultPageSize: 10, defaultPage: 3 }),
|
|
525
|
+
);
|
|
526
|
+
expect(result.current.hasPrevPage).toBe(!result.current.isFirstPage);
|
|
527
|
+
});
|
|
87
528
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
529
|
+
it("Scenario 30 — hasNextPage is inverse of isLastPage", () => {
|
|
530
|
+
const { result } = renderHook(() =>
|
|
531
|
+
usePagination({ totalItems: 50, defaultPageSize: 10, defaultPage: 3 }),
|
|
532
|
+
);
|
|
533
|
+
expect(result.current.hasNextPage).toBe(!result.current.isLastPage);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("Scenario 31 — range.start and range.end correct for mid-page", () => {
|
|
537
|
+
const { result } = renderHook(() =>
|
|
538
|
+
usePagination({ totalItems: 100, defaultPageSize: 10, defaultPage: 2 }),
|
|
539
|
+
);
|
|
540
|
+
expect(result.current.range).toEqual({ start: 11, end: 20 });
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("Scenario 32 — range.end clamped to totalItems on last partial page", () => {
|
|
544
|
+
const { result } = renderHook(() =>
|
|
545
|
+
usePagination({ totalItems: 25, defaultPageSize: 10, defaultPage: 3 }),
|
|
546
|
+
);
|
|
547
|
+
expect(result.current.range).toEqual({ start: 21, end: 25 });
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("Scenario 33 — pageCount = ceil(totalItems / pageSize)", () => {
|
|
551
|
+
const { result } = renderHook(() =>
|
|
552
|
+
usePagination({ totalItems: 25, defaultPageSize: 10 }),
|
|
553
|
+
);
|
|
554
|
+
expect(result.current.pageCount).toBe(3);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
// Scenarios 34–35: Reference Stability
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
describe("usePagination — Reference Stability", () => {
|
|
563
|
+
it("Scenario 34 — action refs are stable across re-renders", () => {
|
|
564
|
+
const { result, rerender } = renderHook(
|
|
565
|
+
(props: { totalItems: number }) => usePagination(props),
|
|
566
|
+
{ initialProps: { totalItems: 100 } },
|
|
567
|
+
);
|
|
568
|
+
const { next, prev, firstPage, lastPage, goTo, setPageSize } =
|
|
569
|
+
result.current;
|
|
570
|
+
rerender({ totalItems: 100 });
|
|
571
|
+
expect(result.current.next).toBe(next);
|
|
572
|
+
expect(result.current.prev).toBe(prev);
|
|
573
|
+
expect(result.current.firstPage).toBe(firstPage);
|
|
574
|
+
expect(result.current.lastPage).toBe(lastPage);
|
|
575
|
+
expect(result.current.goTo).toBe(goTo);
|
|
576
|
+
expect(result.current.setPageSize).toBe(setPageSize);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("Scenario 35 — latest onPageChange is called even after inline function re-creation", () => {
|
|
580
|
+
let callCount = 0;
|
|
581
|
+
let lastCallValue = 0;
|
|
582
|
+
|
|
583
|
+
// Simulate a parent that passes a new inline function on every render
|
|
584
|
+
let currentHandler = (_page: number) => {
|
|
585
|
+
callCount++;
|
|
586
|
+
lastCallValue = _page;
|
|
587
|
+
};
|
|
588
|
+
const { result, rerender } = renderHook(
|
|
589
|
+
({ handler }: { handler: (p: number) => void }) =>
|
|
590
|
+
usePagination({
|
|
591
|
+
totalItems: 50,
|
|
592
|
+
defaultPageSize: 10,
|
|
593
|
+
onPageChange: handler,
|
|
594
|
+
}),
|
|
595
|
+
{ initialProps: { handler: currentHandler } },
|
|
596
|
+
);
|
|
91
597
|
|
|
92
|
-
|
|
598
|
+
// Re-render with a new function reference
|
|
599
|
+
currentHandler = (_page: number) => {
|
|
600
|
+
callCount += 10;
|
|
601
|
+
lastCallValue = _page;
|
|
602
|
+
};
|
|
603
|
+
rerender({ handler: currentHandler });
|
|
93
604
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
605
|
+
act(() => result.current.next());
|
|
606
|
+
// Should call the LATEST handler (callCount would be 10, not 1)
|
|
607
|
+
expect(callCount).toBe(10);
|
|
608
|
+
expect(lastCallValue).toBe(2);
|
|
97
609
|
});
|
|
98
610
|
});
|