@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,434 @@
1
+ /**
2
+ * @file EmptyState Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive test suite for EmptyState 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 } from '@testing-library/react';
13
+ import userEvent from '@testing-library/user-event';
14
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
15
+ import { EmptyState } from '../EmptyState';
16
+
17
+ // Mock lucide-react icons
18
+ vi.mock('lucide-react', () => ({
19
+ Database: ({ className }: { className?: string }) => (
20
+ <div data-testid="lucide-database" className={className}>Database</div>
21
+ ),
22
+ Search: ({ className }: { className?: string }) => (
23
+ <div data-testid="lucide-search" className={className}>Search</div>
24
+ ),
25
+ Plus: ({ className }: { className?: string }) => (
26
+ <div data-testid="lucide-plus" className={className}>Plus</div>
27
+ ),
28
+ User: ({ className }: { className?: string }) => (
29
+ <div data-testid="lucide-user" className={className}>User</div>
30
+ ),
31
+ }));
32
+
33
+ // Mock Button component
34
+ vi.mock('../../Button/Button', () => ({
35
+ Button: ({ children, onClick, variant, ...props }: any) => (
36
+ <button onClick={onClick} data-variant={variant} {...props}>
37
+ {children}
38
+ </button>
39
+ ),
40
+ }));
41
+
42
+ describe('[component] EmptyState', () => {
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ });
46
+
47
+ afterEach(() => {
48
+ vi.clearAllMocks();
49
+ });
50
+
51
+ describe('Rendering', () => {
52
+ it('renders with default content when no props provided', () => {
53
+ render(<EmptyState />);
54
+
55
+ expect(screen.getByText('No data available')).toBeInTheDocument();
56
+ expect(screen.getByText('Get started by adding your first entry')).toBeInTheDocument();
57
+ expect(screen.getByTestId('lucide-database')).toBeInTheDocument();
58
+ });
59
+
60
+ it('renders with custom title', () => {
61
+ render(<EmptyState title="Custom Title" />);
62
+
63
+ expect(screen.getByText('Custom Title')).toBeInTheDocument();
64
+ expect(screen.queryByText('No data available')).not.toBeInTheDocument();
65
+ });
66
+
67
+ it('renders with custom description', () => {
68
+ render(<EmptyState description="Custom description" />);
69
+
70
+ expect(screen.getByText('Custom description')).toBeInTheDocument();
71
+ expect(screen.queryByText('Get started by adding your first entry')).not.toBeInTheDocument();
72
+ });
73
+
74
+ it('renders with custom icon component', () => {
75
+ const CustomIcon = ({ className }: { className?: string }) => (
76
+ <div data-testid="custom-icon" className={className}>Custom</div>
77
+ );
78
+
79
+ render(<EmptyState icon={CustomIcon} />);
80
+
81
+ expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
82
+ expect(screen.queryByTestId('lucide-database')).not.toBeInTheDocument();
83
+ });
84
+
85
+ it('uses default Database icon when no icon provided', () => {
86
+ render(<EmptyState />);
87
+
88
+ expect(screen.getByTestId('lucide-database')).toBeInTheDocument();
89
+ });
90
+ });
91
+
92
+ describe('Filtered State', () => {
93
+ it('shows filtered state message when isFiltered is true', () => {
94
+ render(<EmptyState isFiltered={true} />);
95
+
96
+ expect(screen.getByText('No results found')).toBeInTheDocument();
97
+ expect(screen.getByText('Try adjusting your search or filter criteria')).toBeInTheDocument();
98
+ });
99
+
100
+ it('shows default state message when isFiltered is false', () => {
101
+ render(<EmptyState isFiltered={false} />);
102
+
103
+ expect(screen.getByText('No data available')).toBeInTheDocument();
104
+ expect(screen.getByText('Get started by adding your first entry')).toBeInTheDocument();
105
+ });
106
+
107
+ it('shows custom title and description even when filtered', () => {
108
+ render(
109
+ <EmptyState
110
+ isFiltered={true}
111
+ title="Custom Filter Title"
112
+ description="Custom filter description"
113
+ />
114
+ );
115
+
116
+ expect(screen.getByText('Custom Filter Title')).toBeInTheDocument();
117
+ expect(screen.getByText('Custom filter description')).toBeInTheDocument();
118
+ });
119
+ });
120
+
121
+ describe('Action Button', () => {
122
+ it('renders action button when action prop provided', () => {
123
+ const handleAction = vi.fn();
124
+
125
+ render(
126
+ <EmptyState
127
+ action={{
128
+ label: 'Create Item',
129
+ onClick: handleAction,
130
+ }}
131
+ />
132
+ );
133
+
134
+ const actionButton = screen.getByRole('button', { name: /Create Item/i });
135
+ expect(actionButton).toBeInTheDocument();
136
+ });
137
+
138
+ it('calls action onClick when action button is clicked', async () => {
139
+ const user = userEvent.setup();
140
+ const handleAction = vi.fn();
141
+
142
+ render(
143
+ <EmptyState
144
+ action={{
145
+ label: 'Create Item',
146
+ onClick: handleAction,
147
+ }}
148
+ />
149
+ );
150
+
151
+ const actionButton = screen.getByRole('button', { name: /Create Item/i });
152
+ await user.click(actionButton);
153
+
154
+ expect(handleAction).toHaveBeenCalledTimes(1);
155
+ });
156
+
157
+ it('renders Plus icon in action button', () => {
158
+ const handleAction = vi.fn();
159
+
160
+ render(
161
+ <EmptyState
162
+ action={{
163
+ label: 'Add New',
164
+ onClick: handleAction,
165
+ }}
166
+ />
167
+ );
168
+
169
+ expect(screen.getByTestId('lucide-plus')).toBeInTheDocument();
170
+ });
171
+
172
+ it('does not render action button when action not provided', () => {
173
+ render(<EmptyState />);
174
+
175
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
176
+ });
177
+ });
178
+
179
+ describe('Clear Filters Button', () => {
180
+ it('renders clear filters button when isFiltered and onClearFilters provided', () => {
181
+ const handleClearFilters = vi.fn();
182
+
183
+ render(
184
+ <EmptyState
185
+ isFiltered={true}
186
+ onClearFilters={handleClearFilters}
187
+ />
188
+ );
189
+
190
+ const clearButton = screen.getByRole('button', { name: /Clear filters/i });
191
+ expect(clearButton).toBeInTheDocument();
192
+ });
193
+
194
+ it('calls onClearFilters when clear filters button is clicked', async () => {
195
+ const user = userEvent.setup();
196
+ const handleClearFilters = vi.fn();
197
+
198
+ render(
199
+ <EmptyState
200
+ isFiltered={true}
201
+ onClearFilters={handleClearFilters}
202
+ />
203
+ );
204
+
205
+ const clearButton = screen.getByRole('button', { name: /Clear filters/i });
206
+ await user.click(clearButton);
207
+
208
+ expect(handleClearFilters).toHaveBeenCalledTimes(1);
209
+ });
210
+
211
+ it('renders Search icon in clear filters button', () => {
212
+ const handleClearFilters = vi.fn();
213
+
214
+ render(
215
+ <EmptyState
216
+ isFiltered={true}
217
+ onClearFilters={handleClearFilters}
218
+ />
219
+ );
220
+
221
+ expect(screen.getByTestId('lucide-search')).toBeInTheDocument();
222
+ });
223
+
224
+ it('does not render clear filters button when not filtered', () => {
225
+ const handleClearFilters = vi.fn();
226
+
227
+ render(
228
+ <EmptyState
229
+ isFiltered={false}
230
+ onClearFilters={handleClearFilters}
231
+ />
232
+ );
233
+
234
+ expect(screen.queryByRole('button', { name: /Clear filters/i })).not.toBeInTheDocument();
235
+ });
236
+
237
+ it('does not render clear filters button when onClearFilters not provided', () => {
238
+ render(<EmptyState isFiltered={true} />);
239
+
240
+ expect(screen.queryByRole('button', { name: /Clear filters/i })).not.toBeInTheDocument();
241
+ });
242
+ });
243
+
244
+ describe('Combined Actions', () => {
245
+ it('renders both action and clear filters buttons when appropriate', () => {
246
+ const handleAction = vi.fn();
247
+ const handleClearFilters = vi.fn();
248
+
249
+ render(
250
+ <EmptyState
251
+ isFiltered={true}
252
+ onClearFilters={handleClearFilters}
253
+ action={{
254
+ label: 'Create Item',
255
+ onClick: handleAction,
256
+ }}
257
+ />
258
+ );
259
+
260
+ expect(screen.getByRole('button', { name: /Clear filters/i })).toBeInTheDocument();
261
+ expect(screen.getByRole('button', { name: /Create Item/i })).toBeInTheDocument();
262
+ });
263
+
264
+ it('calls correct handler for each button', async () => {
265
+ const user = userEvent.setup();
266
+ const handleAction = vi.fn();
267
+ const handleClearFilters = vi.fn();
268
+
269
+ render(
270
+ <EmptyState
271
+ isFiltered={true}
272
+ onClearFilters={handleClearFilters}
273
+ action={{
274
+ label: 'Create Item',
275
+ onClick: handleAction,
276
+ }}
277
+ />
278
+ );
279
+
280
+ await user.click(screen.getByRole('button', { name: /Clear filters/i }));
281
+ await user.click(screen.getByRole('button', { name: /Create Item/i }));
282
+
283
+ expect(handleClearFilters).toHaveBeenCalledTimes(1);
284
+ expect(handleAction).toHaveBeenCalledTimes(1);
285
+ });
286
+ });
287
+
288
+ describe('Accessibility', () => {
289
+ it('has proper ARIA role and live region', () => {
290
+ render(<EmptyState />);
291
+
292
+ const container = screen.getByRole('status');
293
+ expect(container).toBeInTheDocument();
294
+ expect(container).toHaveAttribute('aria-live', 'polite');
295
+ });
296
+
297
+ it('marks icon as decorative with aria-hidden', () => {
298
+ render(<EmptyState />);
299
+
300
+ const icon = screen.getByTestId('lucide-database');
301
+ // Icon is wrapped in a component that may not have aria-hidden directly
302
+ // Check that it exists and is accessible
303
+ expect(icon).toBeInTheDocument();
304
+ });
305
+
306
+ it('uses semantic heading for title', () => {
307
+ render(<EmptyState title="Custom Title" />);
308
+
309
+ const heading = screen.getByRole('heading', { level: 3 });
310
+ expect(heading).toHaveTextContent('Custom Title');
311
+ });
312
+
313
+ it('provides proper button labels', () => {
314
+ const handleAction = vi.fn();
315
+
316
+ render(
317
+ <EmptyState
318
+ action={{
319
+ label: 'Create Item',
320
+ onClick: handleAction,
321
+ }}
322
+ />
323
+ );
324
+
325
+ const button = screen.getByRole('button', { name: /Create Item/i });
326
+ expect(button).toBeInTheDocument();
327
+ });
328
+ });
329
+
330
+ describe('Icon Detection', () => {
331
+ it('identifies Database icon correctly', () => {
332
+ render(<EmptyState />);
333
+
334
+ const icon = screen.getByTestId('lucide-database');
335
+ expect(icon).toBeInTheDocument();
336
+ });
337
+
338
+ it('identifies User icon correctly', () => {
339
+ const UserIcon = () => <div data-testid="lucide-user">User</div>;
340
+
341
+ render(<EmptyState icon={UserIcon} />);
342
+
343
+ const icon = screen.getByTestId('lucide-user');
344
+ expect(icon).toBeInTheDocument();
345
+ });
346
+
347
+ it('uses custom testid for unknown icons', () => {
348
+ const CustomIcon = () => <div>Custom</div>;
349
+
350
+ render(<EmptyState icon={CustomIcon} />);
351
+
352
+ // Should still render but with different testid handling
353
+ const container = screen.getByRole('status');
354
+ expect(container).toBeInTheDocument();
355
+ });
356
+ });
357
+
358
+ describe('Edge Cases', () => {
359
+ it('handles empty title string', () => {
360
+ render(<EmptyState title="" />);
361
+
362
+ // Should use default title
363
+ expect(screen.getByText('No data available')).toBeInTheDocument();
364
+ });
365
+
366
+ it('handles empty description string', () => {
367
+ render(<EmptyState description="" />);
368
+
369
+ // Should use default description
370
+ expect(screen.getByText('Get started by adding your first entry')).toBeInTheDocument();
371
+ });
372
+
373
+ it('handles action with empty label', () => {
374
+ const handleAction = vi.fn();
375
+
376
+ render(
377
+ <EmptyState
378
+ action={{
379
+ label: '',
380
+ onClick: handleAction,
381
+ }}
382
+ />
383
+ );
384
+
385
+ // Button should still render
386
+ const button = screen.getByRole('button');
387
+ expect(button).toBeInTheDocument();
388
+ });
389
+
390
+ it('handles rapid button clicks', async () => {
391
+ const user = userEvent.setup();
392
+ const handleAction = vi.fn();
393
+
394
+ render(
395
+ <EmptyState
396
+ action={{
397
+ label: 'Create Item',
398
+ onClick: handleAction,
399
+ }}
400
+ />
401
+ );
402
+
403
+ const button = screen.getByRole('button', { name: /Create Item/i });
404
+ await user.click(button);
405
+ await user.click(button);
406
+ await user.click(button);
407
+
408
+ expect(handleAction).toHaveBeenCalledTimes(3);
409
+ });
410
+ });
411
+
412
+ describe('Layout and Styling', () => {
413
+ it('renders with centered flex layout', () => {
414
+ render(<EmptyState />);
415
+
416
+ const container = screen.getByRole('status');
417
+ expect(container).toHaveClass('flex', 'flex-col', 'items-center', 'justify-center');
418
+ });
419
+
420
+ it('applies text-center class', () => {
421
+ render(<EmptyState />);
422
+
423
+ const container = screen.getByRole('status');
424
+ expect(container).toHaveClass('text-center');
425
+ });
426
+
427
+ it('applies padding', () => {
428
+ render(<EmptyState />);
429
+
430
+ const container = screen.getByRole('status');
431
+ expect(container).toHaveClass('p-8');
432
+ });
433
+ });
434
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @file LoadingState Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive test suite for LoadingState component following testing guidelines.
8
+ * Tests cover all major functionality and accessibility.
9
+ */
10
+
11
+ import React from 'react';
12
+ import { render, screen } from '@testing-library/react';
13
+ import { describe, it, expect } from 'vitest';
14
+ import { LoadingState } from '../LoadingState';
15
+
16
+ describe('[component] LoadingState', () => {
17
+ describe('Rendering', () => {
18
+ it('renders loading spinner and text', () => {
19
+ render(<LoadingState />);
20
+
21
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
22
+ });
23
+
24
+ it('renders with centered layout', () => {
25
+ render(<LoadingState />);
26
+
27
+ // Find the outer container with text-center and p-8 classes
28
+ const container = screen.getByText('Loading...').parentElement?.parentElement;
29
+ expect(container).toHaveClass('text-center', 'p-8');
30
+ });
31
+
32
+ it('renders with padding', () => {
33
+ render(<LoadingState />);
34
+
35
+ // Find the outer container with p-8 class
36
+ const container = screen.getByText('Loading...').parentElement?.parentElement;
37
+ expect(container).toHaveClass('p-8');
38
+ });
39
+
40
+ it('renders spinner with animation class', () => {
41
+ render(<LoadingState />);
42
+
43
+ const spinner = screen.getByText('Loading...').previousElementSibling;
44
+ expect(spinner).toHaveClass('animate-spin');
45
+ });
46
+
47
+ it('renders flex container with items centered', () => {
48
+ render(<LoadingState />);
49
+
50
+ const flexContainer = screen.getByText('Loading...').parentElement;
51
+ expect(flexContainer).toHaveClass('flex', 'items-center', 'justify-center');
52
+ });
53
+ });
54
+
55
+ describe('Accessibility', () => {
56
+ it('provides aria-live region for loading state', () => {
57
+ render(<LoadingState />);
58
+
59
+ const loadingText = screen.getByText('Loading...');
60
+ expect(loadingText).toHaveAttribute('aria-live', 'polite');
61
+ });
62
+
63
+ it('announces loading state to screen readers', () => {
64
+ render(<LoadingState />);
65
+
66
+ const loadingText = screen.getByText('Loading...');
67
+ expect(loadingText).toBeInTheDocument();
68
+ });
69
+ });
70
+
71
+ describe('Visual Structure', () => {
72
+ it('renders spinner before loading text', () => {
73
+ render(<LoadingState />);
74
+
75
+ const container = screen.getByText('Loading...').parentElement;
76
+ const spinner = container?.firstElementChild;
77
+ const text = container?.lastElementChild;
78
+
79
+ expect(spinner).toBeInTheDocument();
80
+ expect(text).toHaveTextContent('Loading...');
81
+ });
82
+
83
+ it('applies space between spinner and text', () => {
84
+ render(<LoadingState />);
85
+
86
+ const container = screen.getByText('Loading...').parentElement;
87
+ expect(container).toHaveClass('space-x-2');
88
+ });
89
+ });
90
+
91
+ describe('Styling', () => {
92
+ it('applies muted foreground color to text', () => {
93
+ render(<LoadingState />);
94
+
95
+ const text = screen.getByText('Loading...');
96
+ expect(text).toHaveClass('text-muted-foreground');
97
+ });
98
+
99
+ it('spinner has rounded-full class', () => {
100
+ render(<LoadingState />);
101
+
102
+ const spinner = screen.getByText('Loading...').previousElementSibling;
103
+ expect(spinner).toHaveClass('rounded-full');
104
+ });
105
+
106
+ it('spinner has border styling', () => {
107
+ render(<LoadingState />);
108
+
109
+ const spinner = screen.getByText('Loading...').previousElementSibling;
110
+ expect(spinner).toHaveClass('border-b-2', 'border-primary');
111
+ });
112
+
113
+ it('spinner has appropriate size', () => {
114
+ render(<LoadingState />);
115
+
116
+ const spinner = screen.getByText('Loading...').previousElementSibling;
117
+ expect(spinner).toHaveClass('h-6', 'w-6');
118
+ });
119
+ });
120
+ });