@jmruthers/pace-core 0.5.110 → 0.5.111

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 (230) hide show
  1. package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
  2. package/dist/{DataTable-D3BK2FCN.js → DataTable-5W2HVLLV.js} +8 -8
  3. package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
  4. package/dist/{api-PIE4JRFS.js → api-SIZPFBFX.js} +5 -3
  5. package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
  6. package/dist/{chunk-3J5N2T2N.js → chunk-2BIDKXQU.js} +113 -116
  7. package/dist/chunk-2BIDKXQU.js.map +1 -0
  8. package/dist/{chunk-AWK2FAUN.js → chunk-ACYQNYHB.js} +7 -7
  9. package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
  10. package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
  11. package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
  12. package/dist/chunk-IWJYNWXN.js.map +1 -0
  13. package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
  14. package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
  15. package/dist/chunk-MW73E7SP.js.map +1 -0
  16. package/dist/{chunk-XRSP3H52.js → chunk-PXXS26G5.js} +57 -23
  17. package/dist/chunk-PXXS26G5.js.map +1 -0
  18. package/dist/{chunk-HGZSO43Y.js → chunk-TD4BXGPE.js} +4 -4
  19. package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
  20. package/dist/{chunk-HADXAZT3.js → chunk-UGVU7L7N.js} +52 -90
  21. package/dist/chunk-UGVU7L7N.js.map +1 -0
  22. package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
  23. package/dist/chunk-X7SPKHYZ.js.map +1 -0
  24. package/dist/{chunk-7GBEBJLR.js → chunk-ZL45MG76.js} +45 -37
  25. package/dist/chunk-ZL45MG76.js.map +1 -0
  26. package/dist/components.js +10 -10
  27. package/dist/hooks.d.ts +11 -1
  28. package/dist/hooks.js +9 -7
  29. package/dist/hooks.js.map +1 -1
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +13 -13
  32. package/dist/providers.d.ts +2 -2
  33. package/dist/providers.js +2 -2
  34. package/dist/rbac/index.d.ts +13 -8
  35. package/dist/rbac/index.js +9 -9
  36. package/dist/utils.js +1 -1
  37. package/docs/api/classes/ColumnFactory.md +1 -1
  38. package/docs/api/classes/ErrorBoundary.md +1 -1
  39. package/docs/api/classes/InvalidScopeError.md +4 -4
  40. package/docs/api/classes/MissingUserContextError.md +4 -4
  41. package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
  42. package/docs/api/classes/PermissionDeniedError.md +4 -4
  43. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  44. package/docs/api/classes/RBACAuditManager.md +8 -8
  45. package/docs/api/classes/RBACCache.md +8 -8
  46. package/docs/api/classes/RBACEngine.md +4 -4
  47. package/docs/api/classes/RBACError.md +4 -4
  48. package/docs/api/classes/RBACNotInitializedError.md +4 -4
  49. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  50. package/docs/api/classes/StorageUtils.md +1 -1
  51. package/docs/api/enums/FileCategory.md +1 -1
  52. package/docs/api/interfaces/AggregateConfig.md +1 -1
  53. package/docs/api/interfaces/ButtonProps.md +1 -1
  54. package/docs/api/interfaces/CardProps.md +1 -1
  55. package/docs/api/interfaces/ColorPalette.md +1 -1
  56. package/docs/api/interfaces/ColorShade.md +1 -1
  57. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  58. package/docs/api/interfaces/DataRecord.md +1 -1
  59. package/docs/api/interfaces/DataTableAction.md +1 -1
  60. package/docs/api/interfaces/DataTableColumn.md +1 -1
  61. package/docs/api/interfaces/DataTableProps.md +1 -1
  62. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  63. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  64. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  65. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  66. package/docs/api/interfaces/FileMetadata.md +1 -1
  67. package/docs/api/interfaces/FileReference.md +1 -1
  68. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  69. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  70. package/docs/api/interfaces/FileUploadProps.md +1 -1
  71. package/docs/api/interfaces/FooterProps.md +1 -1
  72. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  73. package/docs/api/interfaces/InputProps.md +1 -1
  74. package/docs/api/interfaces/LabelProps.md +1 -1
  75. package/docs/api/interfaces/LoginFormProps.md +1 -1
  76. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  77. package/docs/api/interfaces/NavigationContextType.md +1 -1
  78. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  79. package/docs/api/interfaces/NavigationItem.md +1 -1
  80. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  81. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  82. package/docs/api/interfaces/Organisation.md +1 -1
  83. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  84. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  85. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  86. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  87. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  88. package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
  89. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  90. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  91. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  92. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  93. package/docs/api/interfaces/PaletteData.md +1 -1
  94. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  95. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  96. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  97. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  98. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  99. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  100. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  101. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  102. package/docs/api/interfaces/RBACConfig.md +1 -1
  103. package/docs/api/interfaces/RBACLogger.md +1 -1
  104. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  105. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  106. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  107. package/docs/api/interfaces/RouteConfig.md +19 -6
  108. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  109. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  110. package/docs/api/interfaces/StorageConfig.md +1 -1
  111. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  112. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  113. package/docs/api/interfaces/StorageListOptions.md +1 -1
  114. package/docs/api/interfaces/StorageListResult.md +1 -1
  115. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  116. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  117. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  118. package/docs/api/interfaces/StyleImport.md +1 -1
  119. package/docs/api/interfaces/SwitchProps.md +1 -1
  120. package/docs/api/interfaces/ToastActionElement.md +1 -1
  121. package/docs/api/interfaces/ToastProps.md +1 -1
  122. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  123. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  124. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  125. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  126. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  127. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  128. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  129. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  130. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  131. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  132. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  133. package/docs/api/interfaces/UserEventAccess.md +1 -1
  134. package/docs/api/interfaces/UserMenuProps.md +1 -1
  135. package/docs/api/interfaces/UserProfile.md +1 -1
  136. package/docs/api/modules.md +36 -36
  137. package/docs/api-reference/hooks.md +8 -4
  138. package/docs/architecture/rpc-function-standards.md +3 -1
  139. package/docs/best-practices/common-patterns.md +3 -3
  140. package/docs/best-practices/deployment.md +10 -4
  141. package/docs/best-practices/performance.md +11 -3
  142. package/docs/core-concepts/organisations.md +8 -8
  143. package/docs/core-concepts/permissions.md +133 -72
  144. package/docs/migration/rbac-migration.md +65 -66
  145. package/docs/rbac/advanced-patterns.md +15 -22
  146. package/docs/rbac/examples.md +12 -12
  147. package/docs/rbac/getting-started.md +3 -3
  148. package/docs/rbac/troubleshooting.md +2 -1
  149. package/package.json +1 -1
  150. package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
  151. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
  152. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
  153. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
  154. package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
  155. package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
  156. package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
  157. package/src/components/FileUpload/FileUpload.tsx +2 -8
  158. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
  159. package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
  160. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
  161. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
  162. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
  163. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
  164. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
  165. package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
  166. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
  167. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
  168. package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
  169. package/src/hooks/index.ts +1 -1
  170. package/src/hooks/useFileDisplay.ts +51 -0
  171. package/src/hooks/usePermissionCache.test.ts +112 -68
  172. package/src/hooks/usePermissionCache.ts +55 -15
  173. package/src/rbac/README.md +81 -39
  174. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
  175. package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
  176. package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
  177. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
  178. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
  179. package/src/rbac/adapters.tsx +4 -4
  180. package/src/rbac/api.test.ts +37 -13
  181. package/src/rbac/api.ts +25 -8
  182. package/src/rbac/audit.test.ts +2 -2
  183. package/src/rbac/audit.ts +14 -5
  184. package/src/rbac/cache.test.ts +12 -0
  185. package/src/rbac/cache.ts +29 -9
  186. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
  187. package/src/rbac/components/NavigationGuard.tsx +14 -14
  188. package/src/rbac/components/NavigationProvider.test.tsx +1 -1
  189. package/src/rbac/components/PagePermissionGuard.tsx +4 -3
  190. package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
  191. package/src/rbac/components/PermissionEnforcer.tsx +19 -15
  192. package/src/rbac/components/RoleBasedRouter.tsx +16 -9
  193. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
  194. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
  195. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
  196. package/src/rbac/docs/event-based-apps.md +6 -6
  197. package/src/rbac/engine.ts +12 -2
  198. package/src/rbac/hooks/useCan.test.ts +29 -2
  199. package/src/rbac/hooks/usePermissions.test.ts +25 -25
  200. package/src/rbac/hooks/usePermissions.ts +47 -23
  201. package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
  202. package/src/rbac/hooks/useRBAC.test.ts +3 -40
  203. package/src/rbac/hooks/useRBAC.ts +0 -55
  204. package/src/rbac/hooks/useResolvedScope.ts +23 -31
  205. package/src/rbac/permissions.test.ts +11 -7
  206. package/src/rbac/security.test.ts +2 -2
  207. package/src/rbac/security.ts +22 -7
  208. package/src/rbac/types.test.ts +2 -2
  209. package/src/rbac/types.ts +1 -2
  210. package/src/services/EventService.ts +41 -13
  211. package/src/services/__tests__/EventService.test.ts +25 -4
  212. package/src/services/interfaces/IEventService.ts +1 -0
  213. package/src/utils/file-reference.ts +9 -0
  214. package/dist/chunk-2W4WKJVF.js.map +0 -1
  215. package/dist/chunk-3J5N2T2N.js.map +0 -1
  216. package/dist/chunk-7GBEBJLR.js.map +0 -1
  217. package/dist/chunk-AUXS7XSO.js.map +0 -1
  218. package/dist/chunk-HADXAZT3.js.map +0 -1
  219. package/dist/chunk-Q7APDV6H.js.map +0 -1
  220. package/dist/chunk-XRSP3H52.js.map +0 -1
  221. /package/dist/{DataTable-D3BK2FCN.js.map → DataTable-5W2HVLLV.js.map} +0 -0
  222. /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
  223. /package/dist/{api-PIE4JRFS.js.map → api-SIZPFBFX.js.map} +0 -0
  224. /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
  225. /package/dist/{chunk-AWK2FAUN.js.map → chunk-ACYQNYHB.js.map} +0 -0
  226. /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
  227. /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
  228. /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
  229. /package/dist/{chunk-HGZSO43Y.js.map → chunk-TD4BXGPE.js.map} +0 -0
  230. /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
@@ -0,0 +1,519 @@
1
+ /**
2
+ * @file PaginationControls Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive test suite for PaginationControls component following testing guidelines.
8
+ * Tests cover all major functionality, edge cases, and user interactions.
9
+ */
10
+
11
+ import React from 'react';
12
+ import { render, screen, within, waitFor } from '@testing-library/react';
13
+ import userEvent from '@testing-library/user-event';
14
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
15
+ import { PaginationControls } from '../PaginationControls';
16
+ import type { Table } from '@tanstack/react-table';
17
+ import type { DataRecord, ServerSideResponse } from '../../types';
18
+
19
+ // Mock lucide-react icons - use importOriginal to include ChevronDown for Select
20
+ vi.mock('lucide-react', async () => {
21
+ const actual = await vi.importActual('lucide-react');
22
+ return {
23
+ ...actual,
24
+ ChevronLeft: ({ className }: { className?: string }) => <div data-testid="chevron-left" className={className} />,
25
+ ChevronRight: ({ className }: { className?: string }) => <div data-testid="chevron-right" className={className} />,
26
+ ChevronsLeft: ({ className }: { className?: string }) => <div data-testid="chevrons-left" className={className} />,
27
+ ChevronsRight: ({ className }: { className?: string }) => <div data-testid="chevrons-right" className={className} />,
28
+ Loader2: ({ className }: { className?: string }) => <div data-testid="loader-2" className={className} />,
29
+ Server: ({ className }: { className?: string }) => <div data-testid="server-icon" className={className} />,
30
+ Database: ({ className }: { className?: string }) => <div data-testid="database-icon" className={className} />,
31
+ Zap: ({ className }: { className?: string }) => <div data-testid="zap-icon" className={className} />,
32
+ };
33
+ });
34
+
35
+ // Mock Button component
36
+ vi.mock('../../Button/Button', () => ({
37
+ Button: ({ children, onClick, disabled, 'aria-label': ariaLabel, ...props }: any) => (
38
+ <button onClick={onClick} disabled={disabled} aria-label={ariaLabel} {...props}>
39
+ {children}
40
+ </button>
41
+ ),
42
+ }));
43
+
44
+ // Mock Select components - use importOriginal to avoid missing ChevronDown
45
+ vi.mock('../../Select/Select', async () => {
46
+ const actual = await vi.importActual('../../Select/Select');
47
+ return {
48
+ ...actual,
49
+ Select: ({ children, value, onValueChange, disabled }: any) => (
50
+ <div data-testid="select" data-value={value} data-disabled={disabled}>
51
+ <button onClick={() => onValueChange && onValueChange('20')}>Change</button>
52
+ {children}
53
+ </div>
54
+ ),
55
+ SelectTrigger: ({ children, className, 'aria-label': ariaLabel }: any) => (
56
+ <button data-testid="select-trigger" className={className} aria-label={ariaLabel}>
57
+ {children}
58
+ </button>
59
+ ),
60
+ SelectContent: ({ children }: any) => <div data-testid="select-content">{children}</div>,
61
+ SelectItem: ({ children, value }: any) => <div data-testid={`select-item-${value}`}>{children}</div>,
62
+ SelectValue: () => <span data-testid="select-value">10</span>,
63
+ };
64
+ });
65
+
66
+ // Mock pagination utilities
67
+ const mockPaginationState = {
68
+ currentPageIndex: 0,
69
+ currentPageSize: 10,
70
+ pageCount: 5,
71
+ totalRows: 50,
72
+ canPreviousPage: false,
73
+ canNextPage: true,
74
+ startRow: 1,
75
+ endRow: 10,
76
+ };
77
+
78
+ const mockPaginationActions = {
79
+ goToFirstPage: vi.fn(),
80
+ goToPreviousPage: vi.fn(),
81
+ goToNextPage: vi.fn(),
82
+ goToLastPage: vi.fn(),
83
+ goToPage: vi.fn(),
84
+ setPageSize: vi.fn(),
85
+ };
86
+
87
+ vi.mock('../../utils/paginationUtils', () => ({
88
+ getPaginationBinding: vi.fn(() => ({
89
+ state: mockPaginationState,
90
+ actions: mockPaginationActions,
91
+ })),
92
+ getPageSizeOptions: vi.fn((mode: string, options: number[]) => options || [10, 20, 30, 40, 50]),
93
+ }));
94
+
95
+ interface TestData extends DataRecord {
96
+ id: string;
97
+ name: string;
98
+ }
99
+
100
+ const createMockTable = (overrides: Partial<Table<TestData>> = {}): Table<TestData> => ({
101
+ getState: vi.fn(() => ({
102
+ pagination: { pageIndex: 0, pageSize: 10 },
103
+ sorting: [],
104
+ columnFilters: [],
105
+ columnVisibility: {},
106
+ globalFilter: '',
107
+ grouping: [],
108
+ expanded: {},
109
+ })),
110
+ setPageIndex: vi.fn(),
111
+ setPageSize: vi.fn(),
112
+ previousPage: vi.fn(),
113
+ nextPage: vi.fn(),
114
+ getRowCount: vi.fn(() => 50),
115
+ getCanPreviousPage: vi.fn(() => false),
116
+ getCanNextPage: vi.fn(() => true),
117
+ getPageCount: vi.fn(() => 5),
118
+ ...overrides,
119
+ } as unknown as Table<TestData>);
120
+
121
+ describe('[component] PaginationControls', () => {
122
+ let mockTable: Table<TestData>;
123
+
124
+ beforeEach(() => {
125
+ vi.clearAllMocks();
126
+ mockTable = createMockTable();
127
+ mockPaginationState.currentPageIndex = 0;
128
+ mockPaginationState.currentPageSize = 10;
129
+ mockPaginationState.pageCount = 5;
130
+ mockPaginationState.canPreviousPage = false;
131
+ mockPaginationState.canNextPage = true;
132
+ });
133
+
134
+ afterEach(() => {
135
+ vi.clearAllMocks();
136
+ });
137
+
138
+ describe('Rendering', () => {
139
+ it('renders pagination controls with footer element', () => {
140
+ render(<PaginationControls table={mockTable} />);
141
+
142
+ const footer = document.querySelector('footer[aria-label="pagination"]');
143
+ expect(footer).toBeInTheDocument();
144
+ });
145
+
146
+ it('displays current page information', () => {
147
+ render(<PaginationControls table={mockTable} />);
148
+
149
+ expect(screen.getByText(/Page 1 of 5/i)).toBeInTheDocument();
150
+ });
151
+
152
+ it('renders page size selector', () => {
153
+ render(<PaginationControls table={mockTable} />);
154
+
155
+ expect(screen.getByLabelText('Rows per page')).toBeInTheDocument();
156
+ expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
157
+ });
158
+
159
+ it('renders navigation buttons', () => {
160
+ render(<PaginationControls table={mockTable} />);
161
+
162
+ expect(screen.getByRole('button', { name: 'Go to first page' })).toBeInTheDocument();
163
+ expect(screen.getByRole('button', { name: 'Go to previous page' })).toBeInTheDocument();
164
+ expect(screen.getByRole('button', { name: 'Go to next page' })).toBeInTheDocument();
165
+ expect(screen.getByRole('button', { name: 'Go to last page' })).toBeInTheDocument();
166
+ });
167
+
168
+ it('renders navigation icons', () => {
169
+ render(<PaginationControls table={mockTable} />);
170
+
171
+ expect(screen.getByTestId('chevrons-left')).toBeInTheDocument();
172
+ expect(screen.getByTestId('chevron-left')).toBeInTheDocument();
173
+ expect(screen.getByTestId('chevron-right')).toBeInTheDocument();
174
+ expect(screen.getByTestId('chevrons-right')).toBeInTheDocument();
175
+ });
176
+ });
177
+
178
+ describe('Page Navigation', () => {
179
+ it('calls goToFirstPage when first page button is clicked', async () => {
180
+ const user = userEvent.setup();
181
+ mockPaginationState.canPreviousPage = true;
182
+
183
+ render(<PaginationControls table={mockTable} />);
184
+
185
+ const firstButton = screen.getByRole('button', { name: 'Go to first page' });
186
+ await user.click(firstButton);
187
+
188
+ expect(mockPaginationActions.goToFirstPage).toHaveBeenCalledTimes(1);
189
+ });
190
+
191
+ it('calls goToPreviousPage when previous button is clicked', async () => {
192
+ const user = userEvent.setup();
193
+ mockPaginationState.canPreviousPage = true;
194
+ mockPaginationState.currentPageIndex = 2;
195
+
196
+ render(<PaginationControls table={mockTable} />);
197
+
198
+ const prevButton = screen.getByRole('button', { name: 'Go to previous page' });
199
+ await user.click(prevButton);
200
+
201
+ expect(mockPaginationActions.goToPreviousPage).toHaveBeenCalledTimes(1);
202
+ });
203
+
204
+ it('calls goToNextPage when next button is clicked', async () => {
205
+ const user = userEvent.setup();
206
+
207
+ render(<PaginationControls table={mockTable} />);
208
+
209
+ const nextButton = screen.getByRole('button', { name: 'Go to next page' });
210
+ await user.click(nextButton);
211
+
212
+ expect(mockPaginationActions.goToNextPage).toHaveBeenCalledTimes(1);
213
+ });
214
+
215
+ it('calls goToLastPage when last page button is clicked', async () => {
216
+ const user = userEvent.setup();
217
+ mockPaginationState.canNextPage = true;
218
+
219
+ render(<PaginationControls table={mockTable} />);
220
+
221
+ const lastButton = screen.getByRole('button', { name: 'Go to last page' });
222
+ await user.click(lastButton);
223
+
224
+ expect(mockPaginationActions.goToLastPage).toHaveBeenCalledTimes(1);
225
+ });
226
+
227
+ it('disables first and previous buttons on first page', () => {
228
+ mockPaginationState.canPreviousPage = false;
229
+ mockPaginationState.currentPageIndex = 0;
230
+
231
+ render(<PaginationControls table={mockTable} />);
232
+
233
+ const firstButton = screen.getByRole('button', { name: 'Go to first page' });
234
+ const prevButton = screen.getByRole('button', { name: 'Go to previous page' });
235
+
236
+ expect(firstButton).toBeDisabled();
237
+ expect(prevButton).toBeDisabled();
238
+ });
239
+
240
+ it('disables next and last buttons on last page', () => {
241
+ mockPaginationState.canNextPage = false;
242
+ mockPaginationState.currentPageIndex = 4;
243
+ mockPaginationState.pageCount = 5;
244
+
245
+ render(<PaginationControls table={mockTable} />);
246
+
247
+ const nextButton = screen.getByRole('button', { name: 'Go to next page' });
248
+ const lastButton = screen.getByRole('button', { name: 'Go to last page' });
249
+
250
+ expect(nextButton).toBeDisabled();
251
+ expect(lastButton).toBeDisabled();
252
+ });
253
+
254
+ it('updates page display when page changes', () => {
255
+ mockPaginationState.currentPageIndex = 2;
256
+
257
+ render(<PaginationControls table={mockTable} />);
258
+
259
+ expect(screen.getByText(/Page 3 of 5/i)).toBeInTheDocument();
260
+ });
261
+ });
262
+
263
+ describe('Page Size Selection', () => {
264
+ it('allows changing page size', async () => {
265
+ const user = userEvent.setup();
266
+ const onPageSizeChange = vi.fn();
267
+
268
+ render(
269
+ <PaginationControls
270
+ table={mockTable}
271
+ onPageSizeChange={onPageSizeChange}
272
+ pageSizeOptions={[10, 20, 50]}
273
+ />
274
+ );
275
+
276
+ // Open the select dropdown
277
+ const trigger = screen.getByTestId('select-trigger');
278
+ await user.click(trigger);
279
+
280
+ // Wait for options to be visible and select a different page size
281
+ await waitFor(() => {
282
+ expect(screen.getByText('20')).toBeInTheDocument();
283
+ });
284
+
285
+ const option20 = screen.getByText('20');
286
+ await user.click(option20);
287
+
288
+ expect(mockPaginationActions.setPageSize).toHaveBeenCalled();
289
+ });
290
+
291
+ it('disables page size selector when loading', () => {
292
+ render(<PaginationControls table={mockTable} isLoading={true} />);
293
+
294
+ // When loading, the Select component should be disabled
295
+ // Check the trigger button which has the disabled state
296
+ const selectTrigger = screen.getByTestId('select-trigger');
297
+ expect(selectTrigger).toBeDisabled();
298
+ });
299
+
300
+ it('uses custom page size options', () => {
301
+ render(
302
+ <PaginationControls
303
+ table={mockTable}
304
+ pageSizeOptions={[5, 15, 25]}
305
+ />
306
+ );
307
+
308
+ expect(screen.getByTestId('select-root')).toBeInTheDocument();
309
+ });
310
+ });
311
+
312
+ describe('Performance Mode Indicators', () => {
313
+ it('shows client-side indicator when showPerformanceInfo is true', () => {
314
+ render(
315
+ <PaginationControls
316
+ table={mockTable}
317
+ paginationMode="client"
318
+ showPerformanceInfo={true}
319
+ />
320
+ );
321
+
322
+ expect(screen.getByTestId('zap-icon')).toBeInTheDocument();
323
+ expect(screen.getByText('Client-side')).toBeInTheDocument();
324
+ });
325
+
326
+ it('shows server-side indicator when showPerformanceInfo is true', () => {
327
+ render(
328
+ <PaginationControls
329
+ table={mockTable}
330
+ paginationMode="server"
331
+ showPerformanceInfo={true}
332
+ />
333
+ );
334
+
335
+ expect(screen.getByTestId('server-icon')).toBeInTheDocument();
336
+ expect(screen.getByText('Server-side')).toBeInTheDocument();
337
+ });
338
+
339
+ it('shows hybrid indicator when showPerformanceInfo is true', () => {
340
+ render(
341
+ <PaginationControls
342
+ table={mockTable}
343
+ paginationMode="hybrid"
344
+ showPerformanceInfo={true}
345
+ />
346
+ );
347
+
348
+ expect(screen.getByTestId('database-icon')).toBeInTheDocument();
349
+ expect(screen.getByText('Hybrid')).toBeInTheDocument();
350
+ });
351
+
352
+ it('hides performance info when showPerformanceInfo is false', () => {
353
+ render(
354
+ <PaginationControls
355
+ table={mockTable}
356
+ paginationMode="server"
357
+ showPerformanceInfo={false}
358
+ />
359
+ );
360
+
361
+ expect(screen.queryByTestId('server-icon')).not.toBeInTheDocument();
362
+ expect(screen.queryByText('Server-side')).not.toBeInTheDocument();
363
+ });
364
+ });
365
+
366
+ describe('Loading State', () => {
367
+ it('disables all navigation buttons when loading', () => {
368
+ render(<PaginationControls table={mockTable} isLoading={true} />);
369
+
370
+ const firstButton = screen.getByRole('button', { name: 'Go to first page' });
371
+ const prevButton = screen.getByRole('button', { name: 'Go to previous page' });
372
+ const nextButton = screen.getByRole('button', { name: 'Go to next page' });
373
+ const lastButton = screen.getByRole('button', { name: 'Go to last page' });
374
+
375
+ expect(firstButton).toBeDisabled();
376
+ expect(prevButton).toBeDisabled();
377
+ expect(nextButton).toBeDisabled();
378
+ expect(lastButton).toBeDisabled();
379
+ });
380
+
381
+ it('applies disabled styling to page size selector when loading', () => {
382
+ render(<PaginationControls table={mockTable} isLoading={true} />);
383
+
384
+ const selectTrigger = screen.getByTestId('select-trigger');
385
+ expect(selectTrigger.className).toContain('opacity-50');
386
+ expect(selectTrigger.className).toContain('cursor-not-allowed');
387
+ });
388
+ });
389
+
390
+ describe('Server-side Pagination', () => {
391
+ it('works with server-side data', () => {
392
+ // Update mock to return server-side state
393
+ const serverData: ServerSideResponse<TestData> = {
394
+ data: [],
395
+ totalCount: 100,
396
+ pageIndex: 0,
397
+ pageSize: 10,
398
+ pageCount: 10,
399
+ hasNextPage: true,
400
+ hasPreviousPage: false,
401
+ };
402
+
403
+ // Mock pagination state for server mode
404
+ mockPaginationState.pageCount = 10;
405
+ mockPaginationState.totalRows = 100;
406
+
407
+ render(
408
+ <PaginationControls
409
+ table={mockTable}
410
+ paginationMode="server"
411
+ serverData={serverData}
412
+ />
413
+ );
414
+
415
+ expect(screen.getByText(/Page 1 of 10/i)).toBeInTheDocument();
416
+ });
417
+
418
+ it('calls onPageChange when navigating in server mode', async () => {
419
+ const user = userEvent.setup();
420
+ const onPageChange = vi.fn();
421
+ const serverData: ServerSideResponse<TestData> = {
422
+ data: [],
423
+ totalCount: 100,
424
+ pageIndex: 0,
425
+ pageSize: 10,
426
+ pageCount: 10,
427
+ hasNextPage: true,
428
+ hasPreviousPage: false,
429
+ };
430
+
431
+ render(
432
+ <PaginationControls
433
+ table={mockTable}
434
+ paginationMode="server"
435
+ serverData={serverData}
436
+ onPageChange={onPageChange}
437
+ />
438
+ );
439
+
440
+ const nextButton = screen.getByRole('button', { name: 'Go to next page' });
441
+ await user.click(nextButton);
442
+
443
+ expect(mockPaginationActions.goToNextPage).toHaveBeenCalled();
444
+ });
445
+ });
446
+
447
+ describe('Edge Cases', () => {
448
+ it('handles single page correctly', () => {
449
+ mockPaginationState.pageCount = 1;
450
+ mockPaginationState.currentPageIndex = 0;
451
+ mockPaginationState.canNextPage = false;
452
+
453
+ render(<PaginationControls table={mockTable} />);
454
+
455
+ expect(screen.getByText(/Page 1 of 1/i)).toBeInTheDocument();
456
+ expect(screen.getByRole('button', { name: 'Go to next page' })).toBeDisabled();
457
+ });
458
+
459
+ it('handles zero rows correctly', () => {
460
+ mockPaginationState.totalRows = 0;
461
+ mockPaginationState.pageCount = 1;
462
+
463
+ render(<PaginationControls table={mockTable} />);
464
+
465
+ expect(screen.getByText(/Page 1 of 1/i)).toBeInTheDocument();
466
+ });
467
+
468
+ it('handles undefined pageSizeOptions gracefully', () => {
469
+ render(<PaginationControls table={mockTable} pageSizeOptions={undefined} />);
470
+
471
+ expect(screen.getByTestId('select-root')).toBeInTheDocument();
472
+ });
473
+
474
+ it('handles missing table gracefully', () => {
475
+ expect(() => {
476
+ render(<PaginationControls table={null as any} />);
477
+ }).not.toThrow();
478
+ });
479
+ });
480
+
481
+ describe('Accessibility', () => {
482
+ it('has proper aria-label on footer', () => {
483
+ render(<PaginationControls table={mockTable} />);
484
+
485
+ const footer = document.querySelector('footer[aria-label="pagination"]');
486
+ expect(footer).toBeInTheDocument();
487
+ expect(footer).toHaveAttribute('aria-label', 'pagination');
488
+ });
489
+
490
+ it('provides aria-labels for all navigation buttons', () => {
491
+ render(<PaginationControls table={mockTable} />);
492
+
493
+ expect(screen.getByRole('button', { name: 'Go to first page' })).toBeInTheDocument();
494
+ expect(screen.getByRole('button', { name: 'Go to previous page' })).toBeInTheDocument();
495
+ expect(screen.getByRole('button', { name: 'Go to next page' })).toBeInTheDocument();
496
+ expect(screen.getByRole('button', { name: 'Go to last page' })).toBeInTheDocument();
497
+ });
498
+
499
+ it('provides aria-label for page size selector', () => {
500
+ render(<PaginationControls table={mockTable} />);
501
+
502
+ const selector = screen.getByLabelText('Rows per page');
503
+ expect(selector).toBeInTheDocument();
504
+ });
505
+
506
+ it('maintains keyboard navigation with tabIndex', () => {
507
+ render(<PaginationControls table={mockTable} />);
508
+
509
+ const buttons = screen.getAllByRole('button');
510
+ const navButtons = buttons.filter(btn =>
511
+ btn.getAttribute('aria-label')?.includes('page')
512
+ );
513
+
514
+ navButtons.forEach(button => {
515
+ expect(button).toHaveAttribute('tabIndex', '0');
516
+ });
517
+ });
518
+ });
519
+ });