@arbor-education/design-system.components 0.1.0 → 0.1.2

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.
Files changed (120) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/button/Button.d.ts +1 -0
  3. package/dist/components/button/Button.d.ts.map +1 -1
  4. package/dist/components/button/Button.js +2 -1
  5. package/dist/components/button/Button.js.map +1 -1
  6. package/dist/components/formField/inputs/number/NumberInput.d.ts +2 -0
  7. package/dist/components/formField/inputs/number/NumberInput.d.ts.map +1 -1
  8. package/dist/components/formField/inputs/number/NumberInput.js +9 -4
  9. package/dist/components/formField/inputs/number/NumberInput.js.map +1 -1
  10. package/dist/components/table/BulkActionsDropdown.js +2 -2
  11. package/dist/components/table/BulkActionsDropdown.js.map +1 -1
  12. package/dist/components/table/GridApiContext.d.ts +1 -2
  13. package/dist/components/table/GridApiContext.d.ts.map +1 -1
  14. package/dist/components/table/GridApiContext.js +1 -1
  15. package/dist/components/table/GridApiContext.js.map +1 -1
  16. package/dist/components/table/Table.d.ts +9 -1
  17. package/dist/components/table/Table.d.ts.map +1 -1
  18. package/dist/components/table/Table.js +13 -5
  19. package/dist/components/table/Table.js.map +1 -1
  20. package/dist/components/table/Table.stories.d.ts +5 -1
  21. package/dist/components/table/Table.stories.d.ts.map +1 -1
  22. package/dist/components/table/Table.stories.js +36 -1
  23. package/dist/components/table/Table.stories.js.map +1 -1
  24. package/dist/components/table/Table.test.js +2 -0
  25. package/dist/components/table/Table.test.js.map +1 -1
  26. package/dist/components/table/pagination/PageSizeSelector.d.ts +7 -0
  27. package/dist/components/table/pagination/PageSizeSelector.d.ts.map +1 -0
  28. package/dist/components/table/pagination/PageSizeSelector.js +46 -0
  29. package/dist/components/table/pagination/PageSizeSelector.js.map +1 -0
  30. package/dist/components/table/pagination/Pagination.test.d.ts +2 -0
  31. package/dist/components/table/pagination/Pagination.test.d.ts.map +1 -0
  32. package/dist/components/table/pagination/Pagination.test.js +616 -0
  33. package/dist/components/table/pagination/Pagination.test.js.map +1 -0
  34. package/dist/components/table/pagination/PaginationControls.d.ts +6 -0
  35. package/dist/components/table/pagination/PaginationControls.d.ts.map +1 -0
  36. package/dist/components/table/pagination/PaginationControls.js +47 -0
  37. package/dist/components/table/pagination/PaginationControls.js.map +1 -0
  38. package/dist/components/table/pagination/PaginationPanel.d.ts +9 -0
  39. package/dist/components/table/pagination/PaginationPanel.d.ts.map +1 -0
  40. package/dist/components/table/pagination/PaginationPanel.js +11 -0
  41. package/dist/components/table/pagination/PaginationPanel.js.map +1 -0
  42. package/dist/components/table/pagination/RowCountInfo.d.ts +5 -0
  43. package/dist/components/table/pagination/RowCountInfo.d.ts.map +1 -0
  44. package/dist/components/table/pagination/RowCountInfo.js +53 -0
  45. package/dist/components/table/pagination/RowCountInfo.js.map +1 -0
  46. package/dist/components/tooltip/Tooltip.d.ts +7 -0
  47. package/dist/components/tooltip/Tooltip.d.ts.map +1 -0
  48. package/dist/components/tooltip/Tooltip.js +11 -0
  49. package/dist/components/tooltip/Tooltip.js.map +1 -0
  50. package/dist/components/tooltip/Tooltip.stories.d.ts +10 -0
  51. package/dist/components/tooltip/Tooltip.stories.d.ts.map +1 -0
  52. package/dist/components/tooltip/Tooltip.stories.js +24 -0
  53. package/dist/components/tooltip/Tooltip.stories.js.map +1 -0
  54. package/dist/components/tooltip/Tooltip.test.d.ts +2 -0
  55. package/dist/components/tooltip/Tooltip.test.d.ts.map +1 -0
  56. package/dist/components/tooltip/Tooltip.test.js +23 -0
  57. package/dist/components/tooltip/Tooltip.test.js.map +1 -0
  58. package/dist/components/tooltip/TooltipContent.d.ts +8 -0
  59. package/dist/components/tooltip/TooltipContent.d.ts.map +1 -0
  60. package/dist/components/tooltip/TooltipContent.js +11 -0
  61. package/dist/components/tooltip/TooltipContent.js.map +1 -0
  62. package/dist/components/tooltip/TooltipTrigger.d.ts +3 -0
  63. package/dist/components/tooltip/TooltipTrigger.d.ts.map +1 -0
  64. package/dist/components/tooltip/TooltipTrigger.js +8 -0
  65. package/dist/components/tooltip/TooltipTrigger.js.map +1 -0
  66. package/dist/components/tooltip/TooltipWrapper.d.ts +11 -0
  67. package/dist/components/tooltip/TooltipWrapper.d.ts.map +1 -0
  68. package/dist/components/tooltip/TooltipWrapper.js +8 -0
  69. package/dist/components/tooltip/TooltipWrapper.js.map +1 -0
  70. package/dist/components/tooltip/TooltipWrapper.stories.d.ts +11 -0
  71. package/dist/components/tooltip/TooltipWrapper.stories.d.ts.map +1 -0
  72. package/dist/components/tooltip/TooltipWrapper.stories.js +23 -0
  73. package/dist/components/tooltip/TooltipWrapper.stories.js.map +1 -0
  74. package/dist/components/tooltip/TooltipWrapper.test.d.ts +2 -0
  75. package/dist/components/tooltip/TooltipWrapper.test.d.ts.map +1 -0
  76. package/dist/components/tooltip/TooltipWrapper.test.js +42 -0
  77. package/dist/components/tooltip/TooltipWrapper.test.js.map +1 -0
  78. package/dist/index.css +41 -0
  79. package/dist/index.css.map +1 -1
  80. package/dist/index.d.ts +2 -0
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +2 -0
  83. package/dist/index.js.map +1 -1
  84. package/dist/utils/hooks/useGridApi.d.ts +5 -0
  85. package/dist/utils/hooks/useGridApi.d.ts.map +1 -0
  86. package/dist/utils/hooks/useGridApi.js +13 -0
  87. package/dist/utils/hooks/useGridApi.js.map +1 -0
  88. package/dist/utils/hooks/useIsMounted.d.ts +2 -0
  89. package/dist/utils/hooks/useIsMounted.d.ts.map +1 -0
  90. package/dist/utils/hooks/useIsMounted.js +12 -0
  91. package/dist/utils/hooks/useIsMounted.js.map +1 -0
  92. package/package.json +1 -1
  93. package/release/design-system.components.tgz +0 -0
  94. package/src/components/button/Button.tsx +3 -0
  95. package/src/components/button/button.scss +4 -0
  96. package/src/components/formField/inputs/number/NumberInput.tsx +39 -24
  97. package/src/components/table/BulkActionsDropdown.tsx +2 -2
  98. package/src/components/table/GridApiContext.ts +2 -2
  99. package/src/components/table/Table.stories.tsx +64 -2
  100. package/src/components/table/Table.test.tsx +2 -0
  101. package/src/components/table/Table.tsx +14 -4
  102. package/src/components/table/pagination/PageSizeSelector.tsx +73 -0
  103. package/src/components/table/pagination/Pagination.test.tsx +846 -0
  104. package/src/components/table/pagination/PaginationControls.tsx +116 -0
  105. package/src/components/table/pagination/PaginationPanel.tsx +30 -0
  106. package/src/components/table/pagination/RowCountInfo.tsx +67 -0
  107. package/src/components/table/pagination/pagination.scss +26 -0
  108. package/src/components/tooltip/Tooltip.stories.tsx +35 -0
  109. package/src/components/tooltip/Tooltip.test.tsx +44 -0
  110. package/src/components/tooltip/Tooltip.tsx +18 -0
  111. package/src/components/tooltip/TooltipContent.tsx +40 -0
  112. package/src/components/tooltip/TooltipTrigger.tsx +11 -0
  113. package/src/components/tooltip/TooltipWrapper.stories.tsx +24 -0
  114. package/src/components/tooltip/TooltipWrapper.test.tsx +100 -0
  115. package/src/components/tooltip/TooltipWrapper.tsx +32 -0
  116. package/src/components/tooltip/tooltip.scss +15 -0
  117. package/src/index.scss +2 -0
  118. package/src/index.ts +2 -0
  119. package/src/utils/hooks/useGridApi.ts +15 -0
  120. package/src/utils/hooks/useIsMounted.ts +12 -0
@@ -0,0 +1,846 @@
1
+ import { describe, expect, test, vi, beforeEach } from 'vitest';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import React from 'react';
4
+ import '@testing-library/jest-dom/vitest';
5
+ import userEvent from '@testing-library/user-event';
6
+ import type { AgEvent, GridApi } from 'ag-grid-enterprise';
7
+ import { PaginationControls } from './PaginationControls';
8
+ import { RowCountInfo } from './RowCountInfo';
9
+ import { PageSizeSelector } from './PageSizeSelector';
10
+ import { PaginationPanel } from './PaginationPanel';
11
+ import { GridApiContext } from '../GridApiContext';
12
+
13
+ describe('Table Pagination', () => {
14
+ const createMockGridApi = (overrides?: Partial<GridApi>): Partial<GridApi> => {
15
+ const mockListeners: Record<string, Array<() => void>> = {
16
+ paginationChanged: [],
17
+ filterChanged: [],
18
+ rowDataUpdated: [],
19
+ };
20
+
21
+ const result = {
22
+ getDisplayedRowCount: vi.fn(() => 10),
23
+ paginationGetPageSize: vi.fn(() => 10),
24
+ paginationGetCurrentPage: vi.fn(() => 0),
25
+ paginationGetTotalPages: vi.fn(() => 1),
26
+ paginationGetRowCount: vi.fn(() => 10),
27
+ paginationGoToPage: vi.fn(),
28
+ addEventListener: vi.fn((event: string, callback: () => void) => {
29
+ if (!mockListeners[event]) {
30
+ mockListeners[event] = [];
31
+ }
32
+ mockListeners[event].push(callback);
33
+ }),
34
+ removeEventListener: vi.fn(),
35
+ dispatchEvent: (evtName: string) => {
36
+ mockListeners[evtName]?.forEach((callback) => {
37
+ callback();
38
+ });
39
+ },
40
+ ...overrides,
41
+ } as Partial<GridApi> & {
42
+ mockListeners: typeof mockListeners;
43
+ };
44
+
45
+ return result;
46
+ };
47
+ describe('PaginationControls', () => {
48
+ const renderWithContext = (
49
+ component: React.ReactElement,
50
+ gridApi: Partial<GridApi> | null = null,
51
+ ) => {
52
+ return render(
53
+ <GridApiContext.Provider value={gridApi as GridApi | null}>
54
+ {component}
55
+ </GridApiContext.Provider>,
56
+ );
57
+ };
58
+
59
+ test('renders pagination controls container', () => {
60
+ const { container } = renderWithContext(<PaginationControls totalPages={5} />);
61
+ expect(container.querySelector('.ds-table__pagination-controls')).toBeInTheDocument();
62
+ });
63
+
64
+ test('renders all navigation buttons', () => {
65
+ renderWithContext(<PaginationControls totalPages={5} />);
66
+ expect(screen.getByText('Go to first page')).toBeInTheDocument();
67
+ expect(screen.getByText('Go to previous page')).toBeInTheDocument();
68
+ expect(screen.getByText('Go to next page')).toBeInTheDocument();
69
+ expect(screen.getByText('Go to last page')).toBeInTheDocument();
70
+ });
71
+
72
+ test('renders number input for page navigation', () => {
73
+ renderWithContext(<PaginationControls totalPages={5} />);
74
+ const numberInput = screen.getByLabelText('Current page');
75
+ expect(numberInput).toBeInTheDocument();
76
+ expect(numberInput).toHaveValue('1');
77
+ });
78
+
79
+ test('uses totalPages prop when provided', () => {
80
+ renderWithContext(<PaginationControls totalPages={20} />);
81
+ const numberInput = screen.getByLabelText('Current page');
82
+ expect(numberInput).toHaveAttribute('max', '20');
83
+ });
84
+
85
+ test('uses gridApi paginationGetTotalPages when totalPages prop is not provided', () => {
86
+ const mockGridApi = createMockGridApi({
87
+ paginationGetTotalPages: vi.fn(() => 10),
88
+ });
89
+ renderWithContext(<PaginationControls />, mockGridApi as GridApi);
90
+ const numberInput = screen.getByLabelText('Current page');
91
+ expect(numberInput).toHaveAttribute('max', '10');
92
+ expect(mockGridApi.paginationGetTotalPages).toHaveBeenCalled();
93
+ });
94
+
95
+ test('defaults to 0 total pages when neither prop nor gridApi is provided', () => {
96
+ renderWithContext(<PaginationControls />, null);
97
+ const numberInput = screen.getByLabelText('Current page');
98
+ expect(numberInput).toHaveAttribute('max', '1');
99
+ });
100
+
101
+ test('calls onPageChange when provided and first page button is clicked', async () => {
102
+ const onPageChange = vi.fn();
103
+ renderWithContext(
104
+ <PaginationControls totalPages={5} onPageChange={onPageChange} />,
105
+ );
106
+
107
+ // navigate to second page
108
+ const nextButton = screen.getByText('Go to next page');
109
+ await userEvent.click(nextButton);
110
+ onPageChange.mockClear();
111
+
112
+ const firstPageButton = screen.getByText('Go to first page');
113
+ await userEvent.click(firstPageButton);
114
+ expect(onPageChange).toHaveBeenCalledExactlyOnceWith(0); // page is zero-indexed
115
+ });
116
+
117
+ test('calls gridApi.paginationGoToPage when onPageChange is not provided', async () => {
118
+ const mockGridApi = createMockGridApi();
119
+ renderWithContext(<PaginationControls totalPages={5} />, mockGridApi as GridApi);
120
+ const nextPageButton = screen.getByText('Go to next page');
121
+ await userEvent.click(nextPageButton);
122
+ expect(mockGridApi.paginationGoToPage).toHaveBeenCalledExactlyOnceWith(1); // page 2 (1-indexed) becomes 1 (0-indexed)
123
+ });
124
+
125
+ test('navigates to next or previous page when next or previous button is clicked', async () => {
126
+ const onPageChange = vi.fn();
127
+ renderWithContext(
128
+ <PaginationControls totalPages={5} onPageChange={onPageChange} />,
129
+ );
130
+ const nextPageButton = screen.getByText('Go to next page');
131
+ await userEvent.click(nextPageButton);
132
+ expect(onPageChange).toHaveBeenCalledExactlyOnceWith(1);
133
+
134
+ onPageChange.mockClear();
135
+
136
+ const prevPageButton = screen.getByText('Go to previous page');
137
+ await userEvent.click(prevPageButton);
138
+ expect(onPageChange).toHaveBeenCalledExactlyOnceWith(0);
139
+ });
140
+
141
+ test('navigates to next page when next button is clicked', async () => {
142
+ const onPageChange = vi.fn();
143
+ renderWithContext(
144
+ <PaginationControls totalPages={5} onPageChange={onPageChange} />,
145
+ );
146
+ const nextPageButton = screen.getByText('Go to next page');
147
+ await userEvent.click(nextPageButton);
148
+ expect(onPageChange).toHaveBeenCalledExactlyOnceWith(1);
149
+ });
150
+
151
+ test('navigates to last page when last page button is clicked', async () => {
152
+ const onPageChange = vi.fn();
153
+ renderWithContext(
154
+ <PaginationControls totalPages={5} onPageChange={onPageChange} />,
155
+ );
156
+ const lastPageButton = screen.getByText('Go to last page');
157
+ await userEvent.click(lastPageButton);
158
+ expect(onPageChange).toHaveBeenCalledExactlyOnceWith(4); // page 5 (1-indexed) becomes 4 (0-indexed)
159
+ });
160
+
161
+ test('updates input value when navigating via buttons', async () => {
162
+ const onPageChange = vi.fn();
163
+ renderWithContext(
164
+ <PaginationControls totalPages={5} onPageChange={onPageChange} />,
165
+ );
166
+ const numberInput = screen.getByLabelText('Current page');
167
+ const nextPageButton = screen.getByText('Go to next page');
168
+
169
+ expect(numberInput).toHaveValue('1');
170
+ await userEvent.click(nextPageButton);
171
+ expect(numberInput).toHaveValue('2');
172
+ });
173
+
174
+ test('navigates to page when number input value changes', async () => {
175
+ const onPageChange = vi.fn();
176
+ renderWithContext(
177
+ <PaginationControls totalPages={5} onPageChange={onPageChange} />,
178
+ );
179
+ const numberInput = screen.getByLabelText('Current page');
180
+
181
+ await userEvent.clear(numberInput);
182
+ await userEvent.type(numberInput, '3');
183
+ await userEvent.tab(); // Trigger onChange
184
+
185
+ expect(onPageChange).toHaveBeenCalledExactlyOnceWith(2); // page 3 (1-indexed) becomes 2 (0-indexed)
186
+ expect(numberInput).toHaveValue('3');
187
+ });
188
+
189
+ // TODO sort this out when we decide how number input handles inputs outside its min/max range
190
+ test.skip('does not navigate when number input value is 0 or negative', async () => {
191
+ const onPageChange = vi.fn();
192
+ renderWithContext(
193
+ <PaginationControls totalPages={5} onPageChange={onPageChange} />,
194
+ );
195
+ const numberInput = screen.getByLabelText('Current page');
196
+
197
+ await userEvent.clear(numberInput);
198
+ await userEvent.type(numberInput, '0');
199
+ await userEvent.tab();
200
+
201
+ // Should not call onPageChange for invalid values
202
+ expect(onPageChange).not.toHaveBeenCalled();
203
+ });
204
+
205
+ test('disables first and previous buttons on page 1', () => {
206
+ renderWithContext(<PaginationControls totalPages={5} />);
207
+ const firstPageButton = screen.getByText('Go to first page');
208
+ const prevPageButton = screen.getByText('Go to previous page');
209
+
210
+ expect(firstPageButton.parentElement).toBeDisabled();
211
+ expect(prevPageButton.parentElement).toBeDisabled();
212
+ });
213
+
214
+ test('disables next and last buttons on last page', async () => {
215
+ const onPageChange = vi.fn();
216
+ renderWithContext(
217
+ <PaginationControls totalPages={5} onPageChange={onPageChange} />,
218
+ );
219
+ const lastPageButton = screen.getByText('Go to last page');
220
+ const nextPageButton = screen.getByText('Go to next page');
221
+
222
+ // Navigate to last page
223
+ await userEvent.click(lastPageButton);
224
+
225
+ expect(nextPageButton.parentElement).toBeDisabled();
226
+ expect(lastPageButton.parentElement).toBeDisabled();
227
+ });
228
+
229
+ test('enables all buttons on middle pages', async () => {
230
+ const onPageChange = vi.fn();
231
+ renderWithContext(
232
+ <PaginationControls totalPages={5} onPageChange={onPageChange} />,
233
+ );
234
+ const numberInput = screen.getByLabelText('Current page');
235
+
236
+ // Navigate to page 3
237
+ await userEvent.clear(numberInput);
238
+ await userEvent.type(numberInput, '3');
239
+ await userEvent.tab();
240
+
241
+ const firstPageButton = screen.getByText('Go to first page');
242
+ const prevPageButton = screen.getByText('Go to previous page');
243
+ const nextPageButton = screen.getByText('Go to next page');
244
+ const lastPageButton = screen.getByText('Go to last page');
245
+
246
+ expect(firstPageButton).not.toBeDisabled();
247
+ expect(prevPageButton).not.toBeDisabled();
248
+ expect(nextPageButton).not.toBeDisabled();
249
+ expect(lastPageButton).not.toBeDisabled();
250
+ });
251
+
252
+ test('prefers totalPages prop over gridApi when both are provided', () => {
253
+ const mockGridApi = createMockGridApi();
254
+ renderWithContext(
255
+ <PaginationControls totalPages={15} />,
256
+ mockGridApi as GridApi,
257
+ );
258
+ const numberInput = screen.getByLabelText('Current page');
259
+ expect(numberInput).toHaveAttribute('max', '15');
260
+ // gridApi.paginationGetTotalPages should not be called when prop is provided
261
+ expect(mockGridApi.paginationGetTotalPages).not.toHaveBeenCalled();
262
+ });
263
+
264
+ test('handles single page scenario correctly', () => {
265
+ renderWithContext(<PaginationControls totalPages={1} />);
266
+ const firstPageButton = screen.getByText('Go to first page');
267
+ const prevPageButton = screen.getByText('Go to previous page');
268
+ const nextPageButton = screen.getByText('Go to next page');
269
+ const lastPageButton = screen.getByText('Go to last page');
270
+
271
+ expect(screen.getByLabelText('Current page')).toHaveValue('1');
272
+
273
+ expect(firstPageButton.parentElement).toBeDisabled();
274
+ expect(prevPageButton.parentElement).toBeDisabled();
275
+ expect(nextPageButton.parentElement).toBeDisabled();
276
+ expect(lastPageButton.parentElement).toBeDisabled();
277
+ });
278
+ });
279
+
280
+ describe('RowCountInfo', () => {
281
+ const renderWithContext = (
282
+ component: React.ReactElement,
283
+ gridApi: Partial<GridApi> | null = null,
284
+ ) => {
285
+ return render(
286
+ <GridApiContext.Provider value={gridApi as GridApi | null}>
287
+ {component}
288
+ </GridApiContext.Provider>,
289
+ );
290
+ };
291
+
292
+ beforeEach(() => {
293
+ vi.clearAllMocks();
294
+ });
295
+
296
+ test('renders no message when no gridApi is provided', () => {
297
+ renderWithContext(<RowCountInfo />, null);
298
+ expect(screen.queryByText(/Showing \d+ results/)).not.toBeInTheDocument();
299
+ });
300
+
301
+ test('renders message with displayed count and totalRows when totalRows prop is provided', async () => {
302
+ const mockGridApi = createMockGridApi({
303
+ paginationGetTotalPages: vi.fn(() => 5),
304
+ paginationGetRowCount: vi.fn(() => 50),
305
+ });
306
+
307
+ renderWithContext(<RowCountInfo totalRows={100} />, mockGridApi as GridApi);
308
+ await waitFor(() => {
309
+ expect(screen.getByText(/Showing \d+ of 100 results/)).toBeInTheDocument();
310
+ });
311
+ });
312
+
313
+ test('displays correct count when gridApi is ready with single page', async () => {
314
+ const mockGridApi = createMockGridApi({
315
+ getDisplayedRowCount: vi.fn(() => 5),
316
+ paginationGetPageSize: vi.fn(() => 10),
317
+ paginationGetCurrentPage: vi.fn(() => 0),
318
+ paginationGetTotalPages: vi.fn(() => 1),
319
+ });
320
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
321
+
322
+ await waitFor(() => {
323
+ expect(screen.getByText('Showing 5 results')).toBeInTheDocument();
324
+ });
325
+ });
326
+
327
+ test('displays correct count when gridApi is ready with multiple pages', async () => {
328
+ const mockGridApi = createMockGridApi({
329
+ getDisplayedRowCount: vi.fn(() => 10),
330
+ paginationGetPageSize: vi.fn(() => 10),
331
+ paginationGetCurrentPage: vi.fn(() => 0),
332
+ paginationGetTotalPages: vi.fn(() => 5),
333
+ paginationGetRowCount: vi.fn(() => 50),
334
+ });
335
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
336
+
337
+ await waitFor(() => {
338
+ expect(screen.getByText('Showing 10 of 50 results')).toBeInTheDocument();
339
+ });
340
+ });
341
+
342
+ test('displays correct count on last page with remainder', async () => {
343
+ const mockGridApi = createMockGridApi({
344
+ getDisplayedRowCount: vi.fn(() => 7),
345
+ paginationGetPageSize: vi.fn(() => 10),
346
+ paginationGetCurrentPage: vi.fn(() => 4), // page 5 (0-indexed)
347
+ paginationGetTotalPages: vi.fn(() => 5),
348
+ paginationGetRowCount: vi.fn(() => 47),
349
+ });
350
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
351
+
352
+ await waitFor(() => {
353
+ expect(screen.getByText('Showing 7 of 47 results')).toBeInTheDocument();
354
+ });
355
+ });
356
+
357
+ test('displays correct count on last page when displayed count equals page size', async () => {
358
+ const mockGridApi = createMockGridApi({
359
+ getDisplayedRowCount: vi.fn(() => 10),
360
+ paginationGetPageSize: vi.fn(() => 10),
361
+ paginationGetCurrentPage: vi.fn(() => 4), // page 5 (0-indexed)
362
+ paginationGetTotalPages: vi.fn(() => 5),
363
+ paginationGetRowCount: vi.fn(() => 50),
364
+ });
365
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
366
+
367
+ await waitFor(() => {
368
+ expect(screen.getByText('Showing 10 of 50 results')).toBeInTheDocument();
369
+ });
370
+ });
371
+
372
+ test('displays correct count when displayed count is less than page size on non-last page', async () => {
373
+ const mockGridApi = createMockGridApi({
374
+ getDisplayedRowCount: vi.fn(() => 5),
375
+ paginationGetPageSize: vi.fn(() => 10),
376
+ paginationGetCurrentPage: vi.fn(() => 0),
377
+ paginationGetTotalPages: vi.fn(() => 3),
378
+ paginationGetRowCount: vi.fn(() => 25),
379
+ });
380
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
381
+
382
+ await waitFor(() => {
383
+ expect(screen.getByText('Showing 5 of 25 results')).toBeInTheDocument();
384
+ });
385
+ });
386
+
387
+ test('registers event listeners for paginationChanged, filterChanged, and rowDataUpdated', async () => {
388
+ const mockGridApi = createMockGridApi();
389
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
390
+
391
+ await waitFor(() => {
392
+ expect(mockGridApi.addEventListener).toHaveBeenCalledWith('paginationChanged', expect.any(Function));
393
+ expect(mockGridApi.addEventListener).toHaveBeenCalledWith('filterChanged', expect.any(Function));
394
+ expect(mockGridApi.addEventListener).toHaveBeenCalledWith('rowDataUpdated', expect.any(Function));
395
+ });
396
+ });
397
+
398
+ test('updates displayed count when paginationChanged event is triggered', async () => {
399
+ const mockGridApi = createMockGridApi({
400
+ getDisplayedRowCount: vi.fn(() => 10),
401
+ paginationGetPageSize: vi.fn(() => 10),
402
+ paginationGetCurrentPage: vi.fn(() => 0),
403
+ paginationGetTotalPages: vi.fn(() => 2),
404
+ paginationGetRowCount: vi.fn(() => 20),
405
+ });
406
+
407
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
408
+
409
+ await waitFor(() => {
410
+ expect(screen.getByText('Showing 10 of 20 results')).toBeInTheDocument();
411
+ });
412
+
413
+ // Update the mock to return different values
414
+ (mockGridApi.getDisplayedRowCount as ReturnType<typeof vi.fn>).mockReturnValue(5);
415
+ (mockGridApi.paginationGetCurrentPage as ReturnType<typeof vi.fn>).mockReturnValue(1);
416
+
417
+ // Trigger paginationChanged event
418
+ const evtName = 'paginationChanged' as unknown as AgEvent<string>;
419
+ mockGridApi?.dispatchEvent?.(evtName);
420
+
421
+ await waitFor(() => {
422
+ expect(screen.getByText('Showing 5 of 20 results')).toBeInTheDocument();
423
+ });
424
+ });
425
+
426
+ test('updates displayed count when filterChanged event is triggered', async () => {
427
+ const mockGridApi = createMockGridApi();
428
+
429
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
430
+
431
+ await waitFor(() => {
432
+ expect(screen.getByText('Showing 10 results')).toBeInTheDocument();
433
+ });
434
+
435
+ // Update the mock to return different values
436
+ (mockGridApi.getDisplayedRowCount as ReturnType<typeof vi.fn>).mockReturnValue(3);
437
+
438
+ // Trigger filterChanged event
439
+ const evtName = 'filterChanged' as unknown as AgEvent<string>;
440
+ mockGridApi?.dispatchEvent?.(evtName);
441
+
442
+ await waitFor(() => {
443
+ expect(screen.getByText('Showing 3 results')).toBeInTheDocument();
444
+ });
445
+ });
446
+
447
+ test('updates displayed count when rowDataUpdated event is triggered', async () => {
448
+ const mockGridApi = createMockGridApi();
449
+
450
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
451
+
452
+ await waitFor(() => {
453
+ expect(screen.getByText('Showing 10 results')).toBeInTheDocument();
454
+ });
455
+
456
+ // Update the mock to return different values
457
+ (mockGridApi.getDisplayedRowCount as ReturnType<typeof vi.fn>).mockReturnValue(8);
458
+ // Trigger rowDataUpdated event
459
+ const evtName = 'rowDataUpdated' as unknown as AgEvent<string>;
460
+ mockGridApi?.dispatchEvent?.(evtName);
461
+
462
+ await waitFor(() => {
463
+ expect(screen.getByText('Showing 8 results')).toBeInTheDocument();
464
+ });
465
+ });
466
+
467
+ test('handles case when displayedRowCount is undefined', async () => {
468
+ const mockGridApi = createMockGridApi({
469
+ getDisplayedRowCount: vi.fn(() => 0),
470
+ paginationGetPageSize: vi.fn(() => 10),
471
+ paginationGetCurrentPage: vi.fn(() => 0),
472
+ paginationGetTotalPages: vi.fn(() => 1),
473
+ });
474
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
475
+
476
+ await waitFor(() => {
477
+ // Should show a message, but the count might be -Infinity or NaN
478
+ expect(screen.getByText(/Showing .* results/)).toBeInTheDocument();
479
+ });
480
+ });
481
+
482
+ test('handles case when pageSize is undefined', async () => {
483
+ const mockGridApi = createMockGridApi({
484
+ getDisplayedRowCount: vi.fn(() => 10),
485
+ paginationGetPageSize: vi.fn(() => 0),
486
+ paginationGetCurrentPage: vi.fn(() => 0),
487
+ paginationGetTotalPages: vi.fn(() => 1),
488
+ });
489
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
490
+
491
+ await waitFor(() => {
492
+ expect(screen.getByText(/Showing .* results/)).toBeInTheDocument();
493
+ });
494
+ });
495
+
496
+ test('does not show totalRows when pagination has only one page and totalRows prop is not provided', async () => {
497
+ const mockGridApi = createMockGridApi({
498
+ getDisplayedRowCount: vi.fn(() => 5),
499
+ paginationGetPageSize: vi.fn(() => 10),
500
+ paginationGetCurrentPage: vi.fn(() => 0),
501
+ paginationGetTotalPages: vi.fn(() => 1),
502
+ paginationGetRowCount: vi.fn(() => 5),
503
+ });
504
+ renderWithContext(<RowCountInfo />, mockGridApi as GridApi);
505
+
506
+ await waitFor(() => {
507
+ expect(screen.getByText('Showing 5 results')).toBeInTheDocument();
508
+ expect(screen.queryByText(/of \d+ results/)).not.toBeInTheDocument();
509
+ });
510
+ });
511
+ });
512
+
513
+ describe('PageSizeSelector', () => {
514
+ const renderWithContext = (
515
+ component: React.ReactElement,
516
+ gridApi: Partial<GridApi> | null = null,
517
+ ) => {
518
+ return render(
519
+ <GridApiContext.Provider value={gridApi as GridApi | null}>
520
+ {component}
521
+ </GridApiContext.Provider>,
522
+ );
523
+ };
524
+
525
+ beforeEach(() => {
526
+ vi.clearAllMocks();
527
+ });
528
+
529
+ test('renders with default page size of 100', () => {
530
+ renderWithContext(<PageSizeSelector />);
531
+ expect(screen.getByText('100')).toBeInTheDocument();
532
+ });
533
+
534
+ test('renders with initialPageSize prop', () => {
535
+ renderWithContext(<PageSizeSelector initialPageSize={50} />);
536
+ expect(screen.getByText('50')).toBeInTheDocument();
537
+ });
538
+
539
+ test('renders with initialPageSize of "All"', () => {
540
+ renderWithContext(<PageSizeSelector initialPageSize="All" />);
541
+ expect(screen.getByText('All')).toBeInTheDocument();
542
+ });
543
+
544
+ test('opens dropdown when trigger is clicked', async () => {
545
+ renderWithContext(<PageSizeSelector />);
546
+ const trigger = screen.getByText('100');
547
+ await userEvent.click(trigger);
548
+ expect(await screen.findByText('10')).toBeInTheDocument();
549
+ expect(screen.getByText('20')).toBeInTheDocument();
550
+ });
551
+
552
+ test('renders all default page sizes in dropdown', async () => {
553
+ renderWithContext(<PageSizeSelector />);
554
+ const trigger = screen.getByText('100');
555
+ await userEvent.click(trigger);
556
+
557
+ expect(await screen.findByText('10')).toBeInTheDocument();
558
+ expect(screen.getByText('20')).toBeInTheDocument();
559
+ expect(screen.getByText('30')).toBeInTheDocument();
560
+ expect(screen.getByText('50')).toBeInTheDocument();
561
+ expect(screen.getAllByText('100').length).toBe(2);
562
+ expect(screen.getByText('200')).toBeInTheDocument();
563
+ expect(screen.getByText('300')).toBeInTheDocument();
564
+ expect(screen.getByText('500')).toBeInTheDocument();
565
+ expect(screen.getByText('1000')).toBeInTheDocument();
566
+ expect(screen.getByText('All')).toBeInTheDocument();
567
+ });
568
+
569
+ test('renders custom availableSizes', async () => {
570
+ renderWithContext(
571
+ <PageSizeSelector availableSizes={[5, 10, 25]} />,
572
+ );
573
+ const trigger = screen.getByText('100');
574
+ await userEvent.click(trigger);
575
+
576
+ expect(await screen.findByText('5')).toBeInTheDocument();
577
+ expect(screen.getByText('10')).toBeInTheDocument();
578
+ expect(screen.getByText('25')).toBeInTheDocument();
579
+ expect(screen.queryByText('20')).not.toBeInTheDocument();
580
+ });
581
+
582
+ test('calls onPageSizeChange when page size is selected', async () => {
583
+ const onPageSizeChange = vi.fn();
584
+ renderWithContext(
585
+ <PageSizeSelector onPageSizeChange={onPageSizeChange} />,
586
+ );
587
+
588
+ const trigger = screen.getByText('100');
589
+ await userEvent.click(trigger);
590
+
591
+ const item = await screen.findByText('50');
592
+ await userEvent.click(item);
593
+
594
+ expect(onPageSizeChange).toHaveBeenCalledExactlyOnceWith(50);
595
+ });
596
+
597
+ test('updates displayed page size when selection changes', async () => {
598
+ renderWithContext(<PageSizeSelector />);
599
+
600
+ const trigger = screen.getByText('100');
601
+ await userEvent.click(trigger);
602
+
603
+ const item = await screen.findByText('20');
604
+ await userEvent.click(item);
605
+
606
+ await waitFor(() => {
607
+ expect(screen.getByText('20')).toBeInTheDocument();
608
+ });
609
+ });
610
+
611
+ test('calls gridApi.setGridOption with pagination true and paginationPageSize when number is selected', async () => {
612
+ const mockGridApi = createMockGridApi({
613
+ setGridOption: vi.fn(),
614
+ });
615
+ renderWithContext(<PageSizeSelector />, mockGridApi as GridApi);
616
+
617
+ const trigger = screen.getByText('100');
618
+ await userEvent.click(trigger);
619
+
620
+ const item = await screen.findByText('50');
621
+ await userEvent.click(item);
622
+
623
+ await waitFor(() => {
624
+ expect(mockGridApi.setGridOption).toHaveBeenCalledWith('pagination', true);
625
+ expect(mockGridApi.setGridOption).toHaveBeenCalledWith('paginationPageSize', 50);
626
+ });
627
+ });
628
+
629
+ test('calls gridApi.setGridOption with pagination false when "All" is selected', async () => {
630
+ const mockGridApi = createMockGridApi({
631
+ setGridOption: vi.fn(),
632
+ });
633
+ renderWithContext(<PageSizeSelector />, mockGridApi as GridApi);
634
+
635
+ const trigger = screen.getByText('100');
636
+ await userEvent.click(trigger);
637
+
638
+ const item = await screen.findByText('All');
639
+ await userEvent.click(item);
640
+
641
+ await waitFor(() => {
642
+ expect(mockGridApi.setGridOption).toHaveBeenCalledWith('pagination', false);
643
+ });
644
+ });
645
+
646
+ test('handles "All" option in onPageSizeChange callback', async () => {
647
+ const onPageSizeChange = vi.fn();
648
+ renderWithContext(
649
+ <PageSizeSelector onPageSizeChange={onPageSizeChange} />,
650
+ );
651
+
652
+ const trigger = screen.getByText('100');
653
+ await userEvent.click(trigger);
654
+
655
+ const item = await screen.findByText('All');
656
+ await userEvent.click(item);
657
+
658
+ expect(onPageSizeChange).toHaveBeenCalledExactlyOnceWith('All');
659
+ });
660
+
661
+ test('updates page size when initialPageSize is provided and gridApi is ready', async () => {
662
+ const mockGridApi = createMockGridApi({
663
+ setGridOption: vi.fn(),
664
+ });
665
+ renderWithContext(
666
+ <PageSizeSelector initialPageSize={25} />,
667
+ mockGridApi as GridApi,
668
+ );
669
+
670
+ await waitFor(() => {
671
+ expect(mockGridApi.setGridOption).toHaveBeenCalledWith('pagination', true);
672
+ expect(mockGridApi.setGridOption).toHaveBeenCalledWith('paginationPageSize', 25);
673
+ });
674
+ });
675
+
676
+ test('does not update gridApi when initialPageSize is "All" and gridApi is ready', async () => {
677
+ const mockGridApi = createMockGridApi({
678
+ setGridOption: vi.fn(),
679
+ });
680
+ renderWithContext(
681
+ <PageSizeSelector initialPageSize="All" />,
682
+ mockGridApi as GridApi,
683
+ );
684
+
685
+ await waitFor(() => {
686
+ expect(mockGridApi.setGridOption).toHaveBeenCalledWith('pagination', false);
687
+ });
688
+ });
689
+
690
+ test('does not call gridApi methods when gridApi is not available', async () => {
691
+ const onPageSizeChange = vi.fn();
692
+ renderWithContext(
693
+ <PageSizeSelector onPageSizeChange={onPageSizeChange} />,
694
+ null,
695
+ );
696
+
697
+ const trigger = screen.getByText('100');
698
+ await userEvent.click(trigger);
699
+
700
+ const item = await screen.findByText('50');
701
+ await userEvent.click(item);
702
+
703
+ expect(onPageSizeChange).toHaveBeenCalledExactlyOnceWith(50);
704
+ });
705
+
706
+ test('normalizes string numbers to numbers when updating page size', async () => {
707
+ const mockGridApi = createMockGridApi({
708
+ setGridOption: vi.fn(),
709
+ });
710
+ renderWithContext(
711
+ <PageSizeSelector />,
712
+ mockGridApi as GridApi,
713
+ );
714
+
715
+ const trigger = screen.getByLabelText('Select page size');
716
+ await userEvent.click(trigger);
717
+
718
+ // Click on a numeric option - it should be normalized to a number
719
+ const item = await screen.findByText('10');
720
+ await userEvent.click(item);
721
+
722
+ expect(mockGridApi.setGridOption).toHaveBeenCalledWith('paginationPageSize', 10);
723
+ });
724
+
725
+ test('handles custom availableSizes with "All" option', async () => {
726
+ const onPageSizeChange = vi.fn();
727
+ renderWithContext(
728
+ <PageSizeSelector
729
+ availableSizes={[15, 30, 'All']}
730
+ onPageSizeChange={onPageSizeChange}
731
+ />,
732
+ );
733
+
734
+ const trigger = screen.getByText('100');
735
+ await userEvent.click(trigger);
736
+
737
+ expect(await screen.findByText('15')).toBeInTheDocument();
738
+ expect(screen.getByText('30')).toBeInTheDocument();
739
+ expect(screen.getByText('All')).toBeInTheDocument();
740
+
741
+ const allItem = screen.getByText('All');
742
+ await userEvent.click(allItem);
743
+
744
+ expect(onPageSizeChange).toHaveBeenCalledExactlyOnceWith('All');
745
+ });
746
+ });
747
+
748
+ describe('PaginationPanel', () => {
749
+ const renderWithContext = (
750
+ component: React.ReactElement,
751
+ gridApi: Partial<GridApi> = createMockGridApi({
752
+ getDisplayedRowCount: vi.fn(() => 10),
753
+ paginationGetPageSize: vi.fn(() => 10),
754
+ paginationGetCurrentPage: vi.fn(() => 0),
755
+ paginationGetTotalPages: vi.fn(() => 5),
756
+ paginationGetRowCount: vi.fn(() => 50),
757
+ setGridOption: vi.fn(),
758
+ }),
759
+ ) => {
760
+ return render(
761
+ <GridApiContext.Provider value={gridApi as GridApi | null}>
762
+ {component}
763
+ </GridApiContext.Provider>,
764
+ );
765
+ };
766
+
767
+ beforeEach(() => {
768
+ vi.clearAllMocks();
769
+ });
770
+
771
+ test('renders correctly', () => {
772
+ const { container } = renderWithContext(
773
+ <PaginationPanel />,
774
+ );
775
+ const nav = container.querySelector('nav[aria-label="Pagination"]');
776
+ expect(nav).toBeInTheDocument();
777
+ expect(nav).toHaveClass('ds-table__pagination-panel');
778
+ });
779
+
780
+ test('renders the pagination components', async () => {
781
+ renderWithContext(
782
+ <PaginationPanel />,
783
+ );
784
+ await waitFor(() => {
785
+ expect(screen.getByText(/Showing \d+ of \d+ results/)).toBeInTheDocument();
786
+
787
+ expect(screen.getByText('Go to first page')).toBeInTheDocument();
788
+ expect(screen.getByText('Go to previous page')).toBeInTheDocument();
789
+ expect(screen.getByText('Go to next page')).toBeInTheDocument();
790
+ expect(screen.getByText('Go to last page')).toBeInTheDocument();
791
+
792
+ expect(screen.getByLabelText('Select page size')).toBeInTheDocument();
793
+ });
794
+ });
795
+
796
+ test('passes props to RowCountInfo', async () => {
797
+ renderWithContext(
798
+ <PaginationPanel
799
+ totalRows={100}
800
+ />,
801
+ );
802
+ await waitFor(() => {
803
+ expect(screen.getByText(/Showing \d+ of 100 results/)).toBeInTheDocument();
804
+ });
805
+ });
806
+
807
+ test('passes props to PaginationControls', async () => {
808
+ const onPageChange = vi.fn();
809
+
810
+ renderWithContext(
811
+ <PaginationPanel
812
+ totalPages={20}
813
+ onPageChange={onPageChange}
814
+ />,
815
+ );
816
+ const numberInput = screen.getByLabelText('Current page');
817
+ expect(numberInput).toHaveAttribute('max', '20');
818
+ const nextPageButton = screen.getByText('Go to next page');
819
+ await userEvent.click(nextPageButton);
820
+ expect(onPageChange).toHaveBeenCalledExactlyOnceWith(1);
821
+ });
822
+
823
+ test('passes props to PageSizeSelector', async () => {
824
+ const onPageSizeChange = vi.fn();
825
+
826
+ renderWithContext(
827
+ <PaginationPanel
828
+ availableSizes={[5, 15, 25]}
829
+ initialPageSize={5}
830
+ onPageSizeChange={onPageSizeChange}
831
+ />,
832
+ );
833
+
834
+ const trigger = screen.getByLabelText('Select page size');
835
+ expect(trigger.textContent).toContain('5');
836
+ await userEvent.click(trigger);
837
+ expect(await screen.findByText('15')).toBeInTheDocument();
838
+ expect(screen.getAllByText('5').length).toBe(2);
839
+ expect(screen.getByText('15')).toBeInTheDocument();
840
+ const item = await screen.findByText('25');
841
+ expect(item).toBeInTheDocument();
842
+ await userEvent.click(item);
843
+ expect(onPageSizeChange).toHaveBeenLastCalledWith(25);
844
+ });
845
+ });
846
+ });