@startsimpli/ui 0.1.0

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 (86) hide show
  1. package/README.md +537 -0
  2. package/package.json +80 -0
  3. package/src/components/index.ts +50 -0
  4. package/src/components/navigation/sidebar.tsx +178 -0
  5. package/src/components/ui/accordion.tsx +58 -0
  6. package/src/components/ui/alert.tsx +59 -0
  7. package/src/components/ui/badge.tsx +36 -0
  8. package/src/components/ui/button.tsx +57 -0
  9. package/src/components/ui/calendar.tsx +70 -0
  10. package/src/components/ui/card.tsx +68 -0
  11. package/src/components/ui/checkbox.tsx +30 -0
  12. package/src/components/ui/collapsible.tsx +12 -0
  13. package/src/components/ui/dialog.tsx +122 -0
  14. package/src/components/ui/dropdown-menu.tsx +200 -0
  15. package/src/components/ui/index.ts +24 -0
  16. package/src/components/ui/input.tsx +25 -0
  17. package/src/components/ui/label.tsx +26 -0
  18. package/src/components/ui/popover.tsx +31 -0
  19. package/src/components/ui/progress.tsx +28 -0
  20. package/src/components/ui/scroll-area.tsx +48 -0
  21. package/src/components/ui/select.tsx +160 -0
  22. package/src/components/ui/separator.tsx +31 -0
  23. package/src/components/ui/skeleton.tsx +15 -0
  24. package/src/components/ui/table.tsx +117 -0
  25. package/src/components/ui/tabs.tsx +55 -0
  26. package/src/components/ui/textarea.tsx +24 -0
  27. package/src/components/ui/tooltip.tsx +30 -0
  28. package/src/components/unified-table/UnifiedTable.tsx +553 -0
  29. package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
  30. package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
  31. package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
  32. package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
  33. package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
  34. package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
  35. package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
  36. package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
  37. package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
  38. package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
  39. package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
  40. package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
  41. package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
  42. package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
  43. package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
  44. package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
  45. package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
  46. package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
  47. package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
  48. package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
  49. package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
  50. package/src/components/unified-table/components/MobileView/README.md +411 -0
  51. package/src/components/unified-table/components/MobileView/index.tsx +77 -0
  52. package/src/components/unified-table/components/MobileView/types.ts +77 -0
  53. package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
  54. package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
  55. package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
  56. package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
  57. package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
  58. package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
  59. package/src/components/unified-table/hooks/index.ts +21 -0
  60. package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
  61. package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
  62. package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
  63. package/src/components/unified-table/hooks/useFilters.ts +53 -0
  64. package/src/components/unified-table/hooks/usePagination.ts +120 -0
  65. package/src/components/unified-table/hooks/useResponsive.ts +50 -0
  66. package/src/components/unified-table/hooks/useSelection.ts +152 -0
  67. package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
  68. package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
  69. package/src/components/unified-table/hooks/useTableState.ts +103 -0
  70. package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
  71. package/src/components/unified-table/hooks/useTableURL.ts +301 -0
  72. package/src/components/unified-table/index.ts +16 -0
  73. package/src/components/unified-table/types.ts +393 -0
  74. package/src/components/unified-table/utils/export.ts +236 -0
  75. package/src/components/unified-table/utils/index.ts +4 -0
  76. package/src/components/unified-table/utils/renderers.ts +105 -0
  77. package/src/components/unified-table/utils/themes.ts +87 -0
  78. package/src/components/unified-table/utils/validation.ts +122 -0
  79. package/src/index.ts +6 -0
  80. package/src/lib/utils.ts +1 -0
  81. package/src/theme/contract.ts +46 -0
  82. package/src/theme/index.ts +9 -0
  83. package/src/theme/tailwind.config.js +70 -0
  84. package/src/theme/tailwind.preset.ts +93 -0
  85. package/src/utils/cn.ts +6 -0
  86. package/src/utils/index.ts +91 -0
@@ -0,0 +1,128 @@
1
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { SavedViewsDropdown } from '../../components/Toolbar/SavedViewsDropdown'
4
+ import { SavedView } from '../../types'
5
+
6
+ describe('SavedViewsDropdown', () => {
7
+ const mockViews: SavedView[] = [
8
+ {
9
+ id: 'view-1',
10
+ name: 'Default View',
11
+ isDefault: true,
12
+ createdAt: '2024-01-01',
13
+ columnVisibility: { col1: true, col2: false },
14
+ sortBy: 'name',
15
+ sortDirection: 'asc',
16
+ },
17
+ {
18
+ id: 'view-2',
19
+ name: 'Compact View',
20
+ createdAt: '2024-01-02',
21
+ pageSize: 10,
22
+ },
23
+ ]
24
+
25
+ const defaultProps = {
26
+ views: mockViews,
27
+ currentViewId: null,
28
+ onSaveView: jest.fn().mockResolvedValue({ id: 'new-view', name: 'New View', createdAt: '2024-01-03' }),
29
+ onUpdateView: jest.fn().mockResolvedValue(undefined),
30
+ onDeleteView: jest.fn().mockResolvedValue(undefined),
31
+ onLoadView: jest.fn(),
32
+ getCurrentViewState: jest.fn().mockReturnValue({
33
+ columnVisibility: { col1: true },
34
+ sortBy: 'name',
35
+ sortDirection: 'asc',
36
+ }),
37
+ }
38
+
39
+ beforeEach(() => {
40
+ jest.clearAllMocks()
41
+ })
42
+
43
+ describe('rendering', () => {
44
+ it('should render the views button', () => {
45
+ render(<SavedViewsDropdown {...defaultProps} />)
46
+
47
+ expect(screen.getByRole('button', { name: /views/i })).toBeInTheDocument()
48
+ })
49
+
50
+ it('should show current view name when a view is selected', () => {
51
+ render(<SavedViewsDropdown {...defaultProps} currentViewId="view-1" />)
52
+
53
+ expect(screen.getByRole('button', { name: /default view/i })).toBeInTheDocument()
54
+ })
55
+
56
+ it('should render saved views in dropdown', async () => {
57
+ const user = userEvent.setup()
58
+ render(<SavedViewsDropdown {...defaultProps} />)
59
+
60
+ await user.click(screen.getByRole('button', { name: /views/i }))
61
+
62
+ expect(screen.getByText('Default View')).toBeInTheDocument()
63
+ expect(screen.getByText('Compact View')).toBeInTheDocument()
64
+ })
65
+
66
+ it('should show "No saved views" when views array is empty', async () => {
67
+ const user = userEvent.setup()
68
+ render(<SavedViewsDropdown {...defaultProps} views={[]} />)
69
+
70
+ await user.click(screen.getByRole('button', { name: /views/i }))
71
+
72
+ expect(screen.getByText('No saved views')).toBeInTheDocument()
73
+ })
74
+
75
+ it('should show star icon for default view', async () => {
76
+ const user = userEvent.setup()
77
+ render(<SavedViewsDropdown {...defaultProps} />)
78
+
79
+ await user.click(screen.getByRole('button', { name: /views/i }))
80
+
81
+ // Default view should have a star icon (filled)
82
+ // The star is inside the menu item for Default View
83
+ const menuItems = screen.getAllByRole('menuitem')
84
+ expect(menuItems.length).toBeGreaterThan(0)
85
+ })
86
+ })
87
+
88
+ describe('loading views', () => {
89
+ it('should call onLoadView when clicking a view', async () => {
90
+ const user = userEvent.setup()
91
+ render(<SavedViewsDropdown {...defaultProps} />)
92
+
93
+ await user.click(screen.getByRole('button', { name: /views/i }))
94
+ await user.click(screen.getByText('Compact View'))
95
+
96
+ expect(defaultProps.onLoadView).toHaveBeenCalledWith('view-2')
97
+ })
98
+
99
+ it('should show check icon for current view', async () => {
100
+ const user = userEvent.setup()
101
+ render(<SavedViewsDropdown {...defaultProps} currentViewId="view-1" />)
102
+
103
+ await user.click(screen.getByRole('button', { name: /default view/i }))
104
+
105
+ // The current view should have a check icon
106
+ const menuItems = screen.getAllByRole('menuitem')
107
+ expect(menuItems.length).toBeGreaterThan(0)
108
+ })
109
+ })
110
+
111
+ describe('saving views', () => {
112
+ it('should show "Save Current View" option in dropdown', async () => {
113
+ const user = userEvent.setup()
114
+ render(<SavedViewsDropdown {...defaultProps} />)
115
+
116
+ await user.click(screen.getByRole('button', { name: /views/i }))
117
+
118
+ expect(screen.getByRole('menuitem', { name: /save current view/i })).toBeInTheDocument()
119
+ })
120
+
121
+ it('should include getCurrentViewState in props', () => {
122
+ render(<SavedViewsDropdown {...defaultProps} />)
123
+
124
+ // The component should have been rendered with getCurrentViewState
125
+ expect(defaultProps.getCurrentViewState).toBeDefined()
126
+ })
127
+ })
128
+ })
@@ -0,0 +1,374 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react'
2
+ import { TablePagination } from '../../components/TablePagination'
3
+ import { UsePaginationReturn } from '../../types'
4
+
5
+ describe('TablePagination', () => {
6
+ const createMockPagination = (overrides?: Partial<UsePaginationReturn>): UsePaginationReturn => ({
7
+ currentPage: 1,
8
+ pageSize: 25,
9
+ totalPages: 4,
10
+ totalCount: 100,
11
+ canGoNext: true,
12
+ canGoPrevious: false,
13
+ goToPage: jest.fn(),
14
+ goToFirstPage: jest.fn(),
15
+ goToLastPage: jest.fn(),
16
+ goToNextPage: jest.fn(),
17
+ goToPreviousPage: jest.fn(),
18
+ getPageNumbers: jest.fn(() => [1, 2, 3, 4]),
19
+ getDisplayRange: jest.fn(() => ({ start: 1, end: 25 })),
20
+ ...overrides,
21
+ })
22
+
23
+ describe('Display', () => {
24
+ it('should render item count display', () => {
25
+ const pagination = createMockPagination()
26
+ const { container } = render(<TablePagination pagination={pagination} />)
27
+
28
+ // Find the item count display div by its specific class and verify content
29
+ const itemCountDiv = container.querySelector('.text-muted-foreground')
30
+ expect(itemCountDiv).toBeInTheDocument()
31
+ expect(itemCountDiv?.textContent).toContain('Showing')
32
+ expect(itemCountDiv?.textContent).toContain('1-25')
33
+ expect(itemCountDiv?.textContent).toContain('of')
34
+ expect(itemCountDiv?.textContent).toContain('100')
35
+ })
36
+
37
+ it('should display current page and total pages', () => {
38
+ const pagination = createMockPagination({ currentPage: 2, totalPages: 4 })
39
+ render(<TablePagination pagination={pagination} />)
40
+
41
+ expect(screen.getByText(/Page 2 of 4/)).toBeInTheDocument()
42
+ })
43
+
44
+ it('should show filtered items indicator when totalFilteredCount differs from totalCount', () => {
45
+ const pagination = createMockPagination()
46
+ render(<TablePagination pagination={pagination} totalFilteredCount={50} />)
47
+
48
+ expect(screen.getByText(/filtered items/)).toBeInTheDocument()
49
+ })
50
+
51
+ it('should not show filtered indicator when counts match', () => {
52
+ const pagination = createMockPagination()
53
+ render(<TablePagination pagination={pagination} totalFilteredCount={100} />)
54
+
55
+ expect(screen.queryByText(/filtered items/)).not.toBeInTheDocument()
56
+ })
57
+ })
58
+
59
+ describe('Navigation Buttons', () => {
60
+ it('should render all navigation buttons', () => {
61
+ const pagination = createMockPagination()
62
+ render(<TablePagination pagination={pagination} />)
63
+
64
+ expect(screen.getByTitle('First page')).toBeInTheDocument()
65
+ expect(screen.getByTitle('Previous page')).toBeInTheDocument()
66
+ expect(screen.getByTitle('Next page')).toBeInTheDocument()
67
+ expect(screen.getByTitle('Last page')).toBeInTheDocument()
68
+ })
69
+
70
+ it('should disable first and previous buttons on first page', () => {
71
+ const pagination = createMockPagination({ currentPage: 1, canGoPrevious: false })
72
+ render(<TablePagination pagination={pagination} />)
73
+
74
+ expect(screen.getByTitle('First page')).toBeDisabled()
75
+ expect(screen.getByTitle('Previous page')).toBeDisabled()
76
+ })
77
+
78
+ it('should disable next and last buttons on last page', () => {
79
+ const pagination = createMockPagination({
80
+ currentPage: 4,
81
+ totalPages: 4,
82
+ canGoNext: false,
83
+ canGoPrevious: true,
84
+ })
85
+ render(<TablePagination pagination={pagination} />)
86
+
87
+ expect(screen.getByTitle('Next page')).toBeDisabled()
88
+ expect(screen.getByTitle('Last page')).toBeDisabled()
89
+ })
90
+
91
+ it('should enable all buttons on middle page', () => {
92
+ const pagination = createMockPagination({
93
+ currentPage: 2,
94
+ canGoNext: true,
95
+ canGoPrevious: true,
96
+ })
97
+ render(<TablePagination pagination={pagination} />)
98
+
99
+ expect(screen.getByTitle('First page')).not.toBeDisabled()
100
+ expect(screen.getByTitle('Previous page')).not.toBeDisabled()
101
+ expect(screen.getByTitle('Next page')).not.toBeDisabled()
102
+ expect(screen.getByTitle('Last page')).not.toBeDisabled()
103
+ })
104
+ })
105
+
106
+ describe('Page Number Buttons', () => {
107
+ it('should render page number buttons', () => {
108
+ const pagination = createMockPagination()
109
+ render(<TablePagination pagination={pagination} />)
110
+
111
+ expect(screen.getByRole('button', { name: '1' })).toBeInTheDocument()
112
+ expect(screen.getByRole('button', { name: '2' })).toBeInTheDocument()
113
+ expect(screen.getByRole('button', { name: '3' })).toBeInTheDocument()
114
+ expect(screen.getByRole('button', { name: '4' })).toBeInTheDocument()
115
+ })
116
+
117
+ it('should highlight current page button', () => {
118
+ const pagination = createMockPagination({ currentPage: 2 })
119
+ const { container } = render(<TablePagination pagination={pagination} />)
120
+
121
+ const page2Button = screen.getByRole('button', { name: '2' })
122
+
123
+ // Check if it has the default variant class (which indicates it's active)
124
+ expect(page2Button.className).toContain('bg-')
125
+ })
126
+
127
+ it('should render ellipsis for many pages', () => {
128
+ const pagination = createMockPagination({
129
+ currentPage: 10,
130
+ totalPages: 20,
131
+ getPageNumbers: jest.fn(() => [1, '...', 9, 10, 11, '...', 20]),
132
+ })
133
+ render(<TablePagination pagination={pagination} />)
134
+
135
+ expect(screen.getAllByText('•••')).toHaveLength(2)
136
+ })
137
+
138
+ it('should not render pagination controls for single page', () => {
139
+ const pagination = createMockPagination({
140
+ totalPages: 1,
141
+ canGoNext: false,
142
+ canGoPrevious: false,
143
+ })
144
+ render(<TablePagination pagination={pagination} />)
145
+
146
+ expect(screen.queryByTitle('First page')).not.toBeInTheDocument()
147
+ expect(screen.queryByTitle('Next page')).not.toBeInTheDocument()
148
+ })
149
+ })
150
+
151
+ describe('Navigation Actions', () => {
152
+ it('should call goToFirstPage when first button clicked', () => {
153
+ const pagination = createMockPagination({
154
+ currentPage: 3,
155
+ canGoPrevious: true,
156
+ })
157
+ render(<TablePagination pagination={pagination} />)
158
+
159
+ fireEvent.click(screen.getByTitle('First page'))
160
+ expect(pagination.goToFirstPage).toHaveBeenCalled()
161
+ })
162
+
163
+ it('should call goToPreviousPage when previous button clicked', () => {
164
+ const pagination = createMockPagination({
165
+ currentPage: 2,
166
+ canGoPrevious: true,
167
+ })
168
+ render(<TablePagination pagination={pagination} />)
169
+
170
+ fireEvent.click(screen.getByTitle('Previous page'))
171
+ expect(pagination.goToPreviousPage).toHaveBeenCalled()
172
+ })
173
+
174
+ it('should call goToNextPage when next button clicked', () => {
175
+ const pagination = createMockPagination()
176
+ render(<TablePagination pagination={pagination} />)
177
+
178
+ fireEvent.click(screen.getByTitle('Next page'))
179
+ expect(pagination.goToNextPage).toHaveBeenCalled()
180
+ })
181
+
182
+ it('should call goToLastPage when last button clicked', () => {
183
+ const pagination = createMockPagination()
184
+ render(<TablePagination pagination={pagination} />)
185
+
186
+ fireEvent.click(screen.getByTitle('Last page'))
187
+ expect(pagination.goToLastPage).toHaveBeenCalled()
188
+ })
189
+
190
+ it('should call goToPage with correct page number when page button clicked', () => {
191
+ const pagination = createMockPagination()
192
+ render(<TablePagination pagination={pagination} />)
193
+
194
+ fireEvent.click(screen.getByRole('button', { name: '3' }))
195
+ expect(pagination.goToPage).toHaveBeenCalledWith(3)
196
+ })
197
+ })
198
+
199
+ describe('Select All Pages Prompt', () => {
200
+ it('should show prompt when showSelectAllPrompt is true', () => {
201
+ const pagination = createMockPagination({
202
+ getDisplayRange: jest.fn(() => ({ start: 1, end: 25 })),
203
+ })
204
+ render(
205
+ <TablePagination
206
+ pagination={pagination}
207
+ showSelectAllPrompt={true}
208
+ onSelectAllPages={jest.fn()}
209
+ totalFilteredCount={100}
210
+ />
211
+ )
212
+
213
+ // Check that the select all prompt section is rendered
214
+ expect(screen.getByText(/Select all 100 items/)).toBeInTheDocument()
215
+ })
216
+
217
+ it('should not show prompt when showSelectAllPrompt is false', () => {
218
+ const pagination = createMockPagination()
219
+ render(
220
+ <TablePagination
221
+ pagination={pagination}
222
+ showSelectAllPrompt={false}
223
+ totalFilteredCount={100}
224
+ />
225
+ )
226
+
227
+ expect(screen.queryByText(/Select all/)).not.toBeInTheDocument()
228
+ })
229
+
230
+ it('should not show prompt when totalFilteredCount is not greater than displayed items', () => {
231
+ const pagination = createMockPagination({
232
+ getDisplayRange: jest.fn(() => ({ start: 1, end: 100 })),
233
+ })
234
+ render(
235
+ <TablePagination
236
+ pagination={pagination}
237
+ showSelectAllPrompt={true}
238
+ onSelectAllPages={jest.fn()}
239
+ totalFilteredCount={100}
240
+ />
241
+ )
242
+
243
+ expect(screen.queryByText(/Select all/)).not.toBeInTheDocument()
244
+ })
245
+
246
+ it('should call onSelectAllPages when select all button clicked', () => {
247
+ const onSelectAllPages = jest.fn()
248
+ const pagination = createMockPagination({
249
+ getDisplayRange: jest.fn(() => ({ start: 1, end: 25 })),
250
+ })
251
+ render(
252
+ <TablePagination
253
+ pagination={pagination}
254
+ showSelectAllPrompt={true}
255
+ onSelectAllPages={onSelectAllPages}
256
+ totalFilteredCount={100}
257
+ />
258
+ )
259
+
260
+ const selectAllButton = screen.getByText(/Select all 100 items/)
261
+ fireEvent.click(selectAllButton)
262
+ expect(onSelectAllPages).toHaveBeenCalled()
263
+ })
264
+ })
265
+
266
+ describe('Display Range', () => {
267
+ it('should show correct range for first page', () => {
268
+ const pagination = createMockPagination({
269
+ currentPage: 1,
270
+ getDisplayRange: jest.fn(() => ({ start: 1, end: 25 })),
271
+ })
272
+ render(<TablePagination pagination={pagination} />)
273
+
274
+ expect(screen.getByText(/1-25/)).toBeInTheDocument()
275
+ })
276
+
277
+ it('should show correct range for middle page', () => {
278
+ const pagination = createMockPagination({
279
+ currentPage: 2,
280
+ getDisplayRange: jest.fn(() => ({ start: 26, end: 50 })),
281
+ })
282
+ render(<TablePagination pagination={pagination} />)
283
+
284
+ expect(screen.getByText(/26-50/)).toBeInTheDocument()
285
+ })
286
+
287
+ it('should show correct range for last page with partial data', () => {
288
+ const pagination = createMockPagination({
289
+ currentPage: 4,
290
+ totalCount: 90,
291
+ getDisplayRange: jest.fn(() => ({ start: 76, end: 90 })),
292
+ })
293
+ render(<TablePagination pagination={pagination} />)
294
+
295
+ expect(screen.getByText(/76-90/)).toBeInTheDocument()
296
+ })
297
+ })
298
+
299
+ describe('Accessibility', () => {
300
+ it('should have proper aria labels for navigation buttons', () => {
301
+ const pagination = createMockPagination()
302
+ render(<TablePagination pagination={pagination} />)
303
+
304
+ expect(screen.getByTitle('First page')).toHaveAttribute('title')
305
+ expect(screen.getByTitle('Previous page')).toHaveAttribute('title')
306
+ expect(screen.getByTitle('Next page')).toHaveAttribute('title')
307
+ expect(screen.getByTitle('Last page')).toHaveAttribute('title')
308
+ })
309
+
310
+ it('should render page number buttons as buttons', () => {
311
+ const pagination = createMockPagination()
312
+ render(<TablePagination pagination={pagination} />)
313
+
314
+ const pageButtons = screen.getAllByRole('button')
315
+ expect(pageButtons.length).toBeGreaterThan(0)
316
+ })
317
+ })
318
+
319
+ describe('Edge Cases', () => {
320
+ it('should handle zero total count', () => {
321
+ const pagination = createMockPagination({
322
+ totalCount: 0,
323
+ totalPages: 1,
324
+ getDisplayRange: jest.fn(() => ({ start: 0, end: 0 })),
325
+ })
326
+ render(<TablePagination pagination={pagination} />)
327
+
328
+ expect(screen.getByText(/0-0/)).toBeInTheDocument()
329
+ })
330
+
331
+ it('should handle single item', () => {
332
+ const pagination = createMockPagination({
333
+ totalCount: 1,
334
+ totalPages: 1,
335
+ getDisplayRange: jest.fn(() => ({ start: 1, end: 1 })),
336
+ })
337
+ render(<TablePagination pagination={pagination} />)
338
+
339
+ expect(screen.getByText(/1-1/)).toBeInTheDocument()
340
+ })
341
+
342
+ it('should handle very large page numbers', () => {
343
+ const pagination = createMockPagination({
344
+ currentPage: 999,
345
+ totalPages: 1000,
346
+ getPageNumbers: jest.fn(() => [1, '...', 998, 999, 1000]),
347
+ })
348
+ render(<TablePagination pagination={pagination} />)
349
+
350
+ expect(screen.getByText(/Page 999 of 1000/)).toBeInTheDocument()
351
+ })
352
+ })
353
+
354
+ describe('Custom className', () => {
355
+ it('should apply custom className', () => {
356
+ const pagination = createMockPagination()
357
+ const { container } = render(
358
+ <TablePagination pagination={pagination} className="custom-class" />
359
+ )
360
+
361
+ expect(container.firstChild).toHaveClass('custom-class')
362
+ })
363
+ })
364
+
365
+ describe('Responsive Behavior', () => {
366
+ it('should show page info on small screens', () => {
367
+ const pagination = createMockPagination({ currentPage: 2 })
368
+ render(<TablePagination pagination={pagination} />)
369
+
370
+ // Page info should be present (even if hidden on small screens via CSS)
371
+ expect(screen.getByText(/Page 2 of 4/)).toBeInTheDocument()
372
+ })
373
+ })
374
+ })
@@ -0,0 +1,191 @@
1
+ import { renderHook, act } from '@testing-library/react'
2
+ import { useColumnReorder } from '../../hooks/useColumnReorder'
3
+ import { ColumnConfig } from '../../types'
4
+
5
+ type TestData = { id: string; name: string; value: number }
6
+
7
+ const createColumns = (): ColumnConfig<TestData>[] => [
8
+ { id: 'id', header: 'ID', accessorKey: 'id' },
9
+ { id: 'name', header: 'Name', accessorKey: 'name' },
10
+ { id: 'value', header: 'Value', accessorKey: 'value' },
11
+ ]
12
+
13
+ describe('useColumnReorder', () => {
14
+ describe('initialization', () => {
15
+ it('should return columns in default order when no initial order provided', () => {
16
+ const columns = createColumns()
17
+ const { result } = renderHook(() =>
18
+ useColumnReorder({
19
+ columns,
20
+ enabled: true,
21
+ })
22
+ )
23
+
24
+ expect(result.current.orderedColumns.map(c => c.id)).toEqual(['id', 'name', 'value'])
25
+ expect(result.current.columnOrder).toEqual(['id', 'name', 'value'])
26
+ })
27
+
28
+ it('should apply initial order when provided', () => {
29
+ const columns = createColumns()
30
+ const { result } = renderHook(() =>
31
+ useColumnReorder({
32
+ columns,
33
+ initialOrder: ['value', 'id', 'name'],
34
+ enabled: true,
35
+ })
36
+ )
37
+
38
+ expect(result.current.orderedColumns.map(c => c.id)).toEqual(['value', 'id', 'name'])
39
+ expect(result.current.columnOrder).toEqual(['value', 'id', 'name'])
40
+ })
41
+
42
+ it('should filter out invalid column IDs from initial order', () => {
43
+ const columns = createColumns()
44
+ const { result } = renderHook(() =>
45
+ useColumnReorder({
46
+ columns,
47
+ initialOrder: ['value', 'invalid', 'id', 'name'],
48
+ enabled: true,
49
+ })
50
+ )
51
+
52
+ expect(result.current.orderedColumns.map(c => c.id)).toEqual(['value', 'id', 'name'])
53
+ })
54
+
55
+ it('should append new columns not in initial order', () => {
56
+ const columns = createColumns()
57
+ const { result } = renderHook(() =>
58
+ useColumnReorder({
59
+ columns,
60
+ initialOrder: ['value', 'id'], // 'name' is missing
61
+ enabled: true,
62
+ })
63
+ )
64
+
65
+ expect(result.current.orderedColumns.map(c => c.id)).toEqual(['value', 'id', 'name'])
66
+ })
67
+
68
+ it('should return original columns when disabled', () => {
69
+ const columns = createColumns()
70
+ const { result } = renderHook(() =>
71
+ useColumnReorder({
72
+ columns,
73
+ initialOrder: ['value', 'id', 'name'],
74
+ enabled: false,
75
+ })
76
+ )
77
+
78
+ // When disabled, returns columns in their original order
79
+ expect(result.current.orderedColumns.map(c => c.id)).toEqual(['id', 'name', 'value'])
80
+ })
81
+ })
82
+
83
+ describe('handleDragEnd', () => {
84
+ it('should reorder columns when dragging', () => {
85
+ const columns = createColumns()
86
+ const onOrderChange = jest.fn()
87
+ const { result } = renderHook(() =>
88
+ useColumnReorder({
89
+ columns,
90
+ enabled: true,
91
+ onOrderChange,
92
+ })
93
+ )
94
+
95
+ act(() => {
96
+ result.current.handleDragEnd({
97
+ source: { index: 0, droppableId: 'columns' },
98
+ destination: { index: 2, droppableId: 'columns' },
99
+ draggableId: 'id',
100
+ type: 'DEFAULT',
101
+ mode: 'FLUID',
102
+ reason: 'DROP',
103
+ combine: null,
104
+ })
105
+ })
106
+
107
+ // 'id' moved from index 0 to index 2
108
+ expect(result.current.orderedColumns.map(c => c.id)).toEqual(['name', 'value', 'id'])
109
+ expect(onOrderChange).toHaveBeenCalledWith(['name', 'value', 'id'])
110
+ })
111
+
112
+ it('should not reorder when dropped in same position', () => {
113
+ const columns = createColumns()
114
+ const onOrderChange = jest.fn()
115
+ const { result } = renderHook(() =>
116
+ useColumnReorder({
117
+ columns,
118
+ enabled: true,
119
+ onOrderChange,
120
+ })
121
+ )
122
+
123
+ act(() => {
124
+ result.current.handleDragEnd({
125
+ source: { index: 1, droppableId: 'columns' },
126
+ destination: { index: 1, droppableId: 'columns' },
127
+ draggableId: 'name',
128
+ type: 'DEFAULT',
129
+ mode: 'FLUID',
130
+ reason: 'DROP',
131
+ combine: null,
132
+ })
133
+ })
134
+
135
+ expect(result.current.orderedColumns.map(c => c.id)).toEqual(['id', 'name', 'value'])
136
+ expect(onOrderChange).not.toHaveBeenCalled()
137
+ })
138
+
139
+ it('should not reorder when destination is null (cancelled drag)', () => {
140
+ const columns = createColumns()
141
+ const onOrderChange = jest.fn()
142
+ const { result } = renderHook(() =>
143
+ useColumnReorder({
144
+ columns,
145
+ enabled: true,
146
+ onOrderChange,
147
+ })
148
+ )
149
+
150
+ act(() => {
151
+ result.current.handleDragEnd({
152
+ source: { index: 0, droppableId: 'columns' },
153
+ destination: null,
154
+ draggableId: 'id',
155
+ type: 'DEFAULT',
156
+ mode: 'FLUID',
157
+ reason: 'CANCEL',
158
+ combine: null,
159
+ })
160
+ })
161
+
162
+ expect(result.current.orderedColumns.map(c => c.id)).toEqual(['id', 'name', 'value'])
163
+ expect(onOrderChange).not.toHaveBeenCalled()
164
+ })
165
+ })
166
+
167
+ describe('resetOrder', () => {
168
+ it('should reset to default column order', () => {
169
+ const columns = createColumns()
170
+ const onOrderChange = jest.fn()
171
+ const { result } = renderHook(() =>
172
+ useColumnReorder({
173
+ columns,
174
+ initialOrder: ['value', 'id', 'name'],
175
+ enabled: true,
176
+ onOrderChange,
177
+ })
178
+ )
179
+
180
+ // Verify initial custom order
181
+ expect(result.current.orderedColumns.map(c => c.id)).toEqual(['value', 'id', 'name'])
182
+
183
+ act(() => {
184
+ result.current.resetOrder()
185
+ })
186
+
187
+ expect(result.current.orderedColumns.map(c => c.id)).toEqual(['id', 'name', 'value'])
188
+ expect(onOrderChange).toHaveBeenCalledWith(['id', 'name', 'value'])
189
+ })
190
+ })
191
+ })