@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
@@ -54,13 +54,91 @@ vi.mock('../../providers/UnifiedAuthProvider', () => ({
54
54
  useUnifiedAuth: vi.fn(() => mockUnifiedAuth),
55
55
  }));
56
56
 
57
+ // Mock useOrganisations hook
58
+ const mockSelectedOrganisation = {
59
+ id: 'org-123',
60
+ name: 'Test Organisation',
61
+ display_name: 'Test Organisation',
62
+ slug: 'test-org',
63
+ description: 'Test organisation',
64
+ subscription_tier: 'basic',
65
+ settings: {},
66
+ is_active: true,
67
+ created_at: '2023-01-01T00:00:00Z',
68
+ updated_at: '2023-01-01T00:00:00Z',
69
+ };
70
+
71
+ vi.mock('../../hooks/useOrganisations', () => ({
72
+ useOrganisations: vi.fn(() => ({
73
+ selectedOrganisation: mockSelectedOrganisation,
74
+ organisations: [mockSelectedOrganisation],
75
+ userMemberships: [],
76
+ isLoading: false,
77
+ error: null,
78
+ hasValidOrganisationContext: true,
79
+ setSelectedOrganisation: vi.fn(),
80
+ switchOrganisation: vi.fn().mockResolvedValue(undefined),
81
+ getUserRole: vi.fn().mockReturnValue('member'),
82
+ validateOrganisationAccess: vi.fn().mockReturnValue(true),
83
+ ensureOrganisationContext: vi.fn().mockReturnValue(mockSelectedOrganisation),
84
+ refreshOrganisations: vi.fn().mockResolvedValue(undefined),
85
+ getPrimaryOrganisation: vi.fn().mockReturnValue(mockSelectedOrganisation),
86
+ isOrganisationSecure: vi.fn().mockReturnValue(true),
87
+ })),
88
+ }));
89
+
90
+ // Mock useEvents hook (optional - wrapped in try/catch in component)
91
+ vi.mock('../../providers/EventsProvider', () => ({
92
+ useEvents: vi.fn(() => ({
93
+ selectedEvent: { event_id: 'event-123' },
94
+ events: [],
95
+ isLoading: false,
96
+ error: null,
97
+ })),
98
+ }));
99
+
57
100
  // Mock RBAC functions
101
+ const mockIsPermitted = vi.fn().mockResolvedValue(true);
102
+ const mockIsPermittedCached = vi.fn().mockResolvedValue(true);
103
+
58
104
  vi.mock('../../rbac/api', () => ({
59
- isPermitted: vi.fn().mockResolvedValue(true),
105
+ isPermitted: vi.fn(),
106
+ isPermittedCached: vi.fn(),
60
107
  isSuperAdmin: vi.fn().mockResolvedValue(false),
61
108
  setupRBAC: vi.fn(),
62
109
  }));
63
110
 
111
+ // Mock useCan hook - this is what PaceAppLayout actually uses
112
+ const mockUseCan = vi.fn(() => ({
113
+ can: true,
114
+ isLoading: false,
115
+ error: null,
116
+ refetch: vi.fn().mockResolvedValue(undefined),
117
+ }));
118
+
119
+ // Mock RBAC hooks
120
+ const mockHasPermissionFn = vi.fn().mockResolvedValue(true);
121
+ vi.mock('../../rbac/hooks', () => ({
122
+ useRBAC: vi.fn(() => ({
123
+ hasPermission: mockHasPermissionFn,
124
+ isLoading: false,
125
+ error: null,
126
+ hasGlobalPermission: vi.fn().mockResolvedValue(true),
127
+ hasOrganisationPermission: vi.fn().mockResolvedValue(true),
128
+ hasEventPermission: vi.fn().mockResolvedValue(true),
129
+ globalRole: null,
130
+ organisationRoles: [],
131
+ eventRoles: [],
132
+ permissionMap: {},
133
+ })),
134
+ useCan: (...args: any[]) => mockUseCan(...args),
135
+ useResolvedScope: vi.fn(() => ({
136
+ resolvedScope: { organisationId: 'org-123', eventId: 'event-123', appId: 'app-123' },
137
+ isLoading: false,
138
+ error: null,
139
+ })),
140
+ }));
141
+
64
142
  // Mock Header component
65
143
  vi.mock('../Header', () => ({
66
144
  Header: ({
@@ -151,10 +229,21 @@ describe('PaceAppLayout Component', () => {
151
229
  vi.clearAllMocks();
152
230
  // Reset location mock
153
231
  mockLocation.pathname = '/dashboard';
154
- // Reset RBAC mocks
155
- const { isPermitted, isSuperAdmin } = await import('../../rbac/api');
232
+ // Reset RBAC hook mocks
233
+ mockHasPermissionFn.mockClear();
234
+ mockHasPermissionFn.mockResolvedValue(true);
235
+ mockUseCan.mockReturnValue({
236
+ can: true,
237
+ isLoading: false,
238
+ error: null,
239
+ refetch: vi.fn().mockResolvedValue(undefined),
240
+ });
241
+ // Reset RBAC API mocks
242
+ const { isPermitted, isPermittedCached, isSuperAdmin } = await import('../../rbac/api');
156
243
  vi.mocked(isPermitted).mockReset();
157
244
  vi.mocked(isPermitted).mockResolvedValue(true);
245
+ vi.mocked(isPermittedCached).mockReset();
246
+ vi.mocked(isPermittedCached).mockResolvedValue(true);
158
247
  vi.mocked(isSuperAdmin).mockReset();
159
248
  vi.mocked(isSuperAdmin).mockResolvedValue(false);
160
249
  });
@@ -360,8 +449,13 @@ describe('PaceAppLayout Component', () => {
360
449
  });
361
450
 
362
451
  it('shows loading state when checking permissions', async () => {
363
- const { isPermitted } = await import('../../rbac/api');
364
- vi.mocked(isPermitted).mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(true), 100)));
452
+ // Mock useCan to return loading state
453
+ mockUseCan.mockReturnValueOnce({
454
+ can: false,
455
+ isLoading: true,
456
+ error: null,
457
+ refetch: vi.fn().mockResolvedValue(undefined),
458
+ });
365
459
 
366
460
  renderWithProviders(
367
461
  <TestWrapper>
@@ -374,8 +468,13 @@ describe('PaceAppLayout Component', () => {
374
468
  });
375
469
 
376
470
  it('shows permission error when check fails', async () => {
377
- const { isPermitted } = await import('../../rbac/api');
378
- vi.mocked(isPermitted).mockRejectedValue(new Error('Permission check failed'));
471
+ // Mock useCan to return an error state
472
+ mockUseCan.mockReturnValueOnce({
473
+ can: false,
474
+ isLoading: false,
475
+ error: new Error('Permission check failed'),
476
+ refetch: vi.fn().mockResolvedValue(undefined),
477
+ });
379
478
 
380
479
  renderWithProviders(
381
480
  <TestWrapper>
@@ -390,8 +489,13 @@ describe('PaceAppLayout Component', () => {
390
489
  });
391
490
 
392
491
  it('shows access denied when user lacks permission', async () => {
393
- const { isPermitted } = await import('../../rbac/api');
394
- vi.mocked(isPermitted).mockResolvedValue(false);
492
+ // Mock useCan to return false (no permission)
493
+ mockUseCan.mockReturnValueOnce({
494
+ can: false,
495
+ isLoading: false,
496
+ error: null,
497
+ refetch: vi.fn().mockResolvedValue(undefined),
498
+ });
395
499
 
396
500
  renderWithProviders(
397
501
  <TestWrapper>
@@ -406,11 +510,16 @@ describe('PaceAppLayout Component', () => {
406
510
  });
407
511
 
408
512
  it('shows custom permission fallback when provided', async () => {
409
- const { isPermitted } = await import('../../rbac/api');
410
- vi.mocked(isPermitted).mockResolvedValue(false);
411
-
513
+ // Arrange
514
+ mockUseCan.mockReturnValueOnce({
515
+ can: false,
516
+ isLoading: false,
517
+ error: null,
518
+ refetch: vi.fn().mockResolvedValue(undefined),
519
+ });
412
520
  const customFallback = <div data-testid="custom-fallback">Custom Access Denied</div>;
413
521
 
522
+ // Act
414
523
  renderWithProviders(
415
524
  <TestWrapper>
416
525
  <PaceAppLayout
@@ -421,17 +530,23 @@ describe('PaceAppLayout Component', () => {
421
530
  </TestWrapper>
422
531
  );
423
532
 
533
+ // Assert - No need to wait for loading since mock returns immediately
424
534
  await waitFor(() => {
425
535
  expect(screen.getByTestId('custom-fallback')).toBeInTheDocument();
426
- }, { timeout: 3000 });
536
+ });
427
537
  });
428
538
 
429
539
  it('shows page permission fallback when enforcePagePermissions is true', async () => {
430
- const { isPermitted } = await import('../../rbac/api');
431
- vi.mocked(isPermitted).mockResolvedValue(false);
432
-
540
+ // Arrange
541
+ mockUseCan.mockReturnValueOnce({
542
+ can: false,
543
+ isLoading: false,
544
+ error: null,
545
+ refetch: vi.fn().mockResolvedValue(undefined),
546
+ });
433
547
  const pageFallback = <div data-testid="page-fallback">Page Access Denied</div>;
434
548
 
549
+ // Act
435
550
  renderWithProviders(
436
551
  <TestWrapper>
437
552
  <PaceAppLayout
@@ -443,9 +558,10 @@ describe('PaceAppLayout Component', () => {
443
558
  </TestWrapper>
444
559
  );
445
560
 
561
+ // Assert - No need to wait for loading since mock returns immediately
446
562
  await waitFor(() => {
447
563
  expect(screen.getByTestId('page-fallback')).toBeInTheDocument();
448
- }, { timeout: 3000 });
564
+ });
449
565
  });
450
566
  });
451
567
 
@@ -514,9 +630,6 @@ describe('PaceAppLayout Component', () => {
514
630
 
515
631
  describe('Route-Specific Permissions', () => {
516
632
  it('uses route-specific permissions when provided', async () => {
517
- const { isPermitted } = await import('../../rbac/api');
518
- vi.mocked(isPermitted).mockResolvedValue(true);
519
-
520
633
  renderWithProviders(
521
634
  <TestWrapper>
522
635
  <PaceAppLayout
@@ -533,23 +646,18 @@ describe('PaceAppLayout Component', () => {
533
646
  );
534
647
 
535
648
  await waitFor(() => {
536
- expect(vi.mocked(isPermitted)).toHaveBeenCalledWith({
537
- userId: 'user-123',
538
- scope: {
539
- organisationId: 'org-123',
540
- eventId: 'event-123',
541
- appId: 'app-123',
542
- },
543
- permission: 'update:page.dashboard-page',
544
- pageId: 'dashboard-page',
545
- });
546
- }, { timeout: 3000 });
547
- });
649
+ // useCan is called with userId, scope, permission, pageId, useCache
650
+ expect(mockUseCan).toHaveBeenCalledWith(
651
+ 'user-123',
652
+ expect.objectContaining({ organisationId: 'org-123' }),
653
+ 'update:page.dashboard-page',
654
+ 'dashboard-page',
655
+ true
656
+ );
657
+ }, { timeout: 5000 });
658
+ }, { timeout: 6000 });
548
659
 
549
660
  it('uses default permission when route not in routePermissions', async () => {
550
- const { isPermitted } = await import('../../rbac/api');
551
- vi.mocked(isPermitted).mockResolvedValue(true);
552
-
553
661
  renderWithProviders(
554
662
  <TestWrapper>
555
663
  <PaceAppLayout
@@ -564,18 +672,17 @@ describe('PaceAppLayout Component', () => {
564
672
  );
565
673
 
566
674
  await waitFor(() => {
567
- expect(vi.mocked(isPermitted)).toHaveBeenCalledWith({
568
- userId: 'user-123',
569
- scope: {
570
- organisationId: 'org-123',
571
- eventId: 'event-123',
572
- appId: 'app-123',
573
- },
574
- permission: 'create:page.dashboard',
575
- pageId: 'dashboard',
576
- });
577
- }, { timeout: 3000 });
578
- });
675
+ // useCan is called with userId, scope, permission, pageId, useCache
676
+ // Uses defaultPermission "create" since /dashboard is not in routePermissions
677
+ expect(mockUseCan).toHaveBeenCalledWith(
678
+ 'user-123',
679
+ expect.objectContaining({ organisationId: 'org-123' }),
680
+ 'create:page.dashboard',
681
+ 'dashboard',
682
+ true
683
+ );
684
+ }, { timeout: 5000 });
685
+ }, { timeout: 6000 });
579
686
  });
580
687
 
581
688
  describe('Super Admin Bypass', () => {
@@ -601,11 +708,16 @@ describe('PaceAppLayout Component', () => {
601
708
 
602
709
  describe('Callbacks and Event Handling', () => {
603
710
  it('calls onPageAccessDenied when access is denied', async () => {
604
- const { isPermitted } = await import('../../rbac/api');
605
- vi.mocked(isPermitted).mockResolvedValue(false);
606
-
711
+ // Arrange
607
712
  const onPageAccessDenied = vi.fn();
713
+ mockUseCan.mockReturnValueOnce({
714
+ can: false,
715
+ isLoading: false,
716
+ error: null,
717
+ refetch: vi.fn().mockResolvedValue(undefined),
718
+ });
608
719
 
720
+ // Act
609
721
  renderWithProviders(
610
722
  <TestWrapper>
611
723
  <PaceAppLayout
@@ -616,17 +728,23 @@ describe('PaceAppLayout Component', () => {
616
728
  </TestWrapper>
617
729
  );
618
730
 
731
+ // Assert - Callback should be called immediately when can is false
619
732
  await waitFor(() => {
620
733
  expect(onPageAccessDenied).toHaveBeenCalledWith('dashboard', 'read');
621
- }, { timeout: 3000 });
734
+ });
622
735
  });
623
736
 
624
737
  it('calls onStrictModeViolation when strict mode is violated', async () => {
625
- const { isPermitted } = await import('../../rbac/api');
626
- vi.mocked(isPermitted).mockResolvedValue(false);
627
-
738
+ // Arrange
628
739
  const onStrictModeViolation = vi.fn();
740
+ mockUseCan.mockReturnValueOnce({
741
+ can: false,
742
+ isLoading: false,
743
+ error: null,
744
+ refetch: vi.fn().mockResolvedValue(undefined),
745
+ });
629
746
 
747
+ // Act
630
748
  renderWithProviders(
631
749
  <TestWrapper>
632
750
  <PaceAppLayout
@@ -638,9 +756,10 @@ describe('PaceAppLayout Component', () => {
638
756
  </TestWrapper>
639
757
  );
640
758
 
759
+ // Assert - Callback should be called immediately when can is false
641
760
  await waitFor(() => {
642
761
  expect(onStrictModeViolation).toHaveBeenCalledWith('dashboard', 'read');
643
- }, { timeout: 3000 });
762
+ });
644
763
  });
645
764
  });
646
765
 
@@ -714,8 +833,14 @@ describe('PaceAppLayout Component', () => {
714
833
  // Mock the useUnifiedAuth hook to return null user
715
834
  vi.mocked(useUnifiedAuth).mockReturnValue(mockAuthWithoutUser);
716
835
 
717
- const { isPermitted } = await import('../../rbac/api');
718
- vi.mocked(isPermitted).mockResolvedValue(false);
836
+ // When there's no user, useCan is called with empty string userId
837
+ // and will return false (no permission)
838
+ mockUseCan.mockReturnValueOnce({
839
+ can: false,
840
+ isLoading: false,
841
+ error: null,
842
+ refetch: vi.fn().mockResolvedValue(undefined),
843
+ });
719
844
 
720
845
  renderWithProviders(
721
846
  <TestWrapper>
@@ -732,6 +857,7 @@ describe('PaceAppLayout Component', () => {
732
857
  });
733
858
 
734
859
  it('handles missing organisation context', async () => {
860
+ // Arrange
735
861
  const mockUserWithoutOrg = {
736
862
  ...mockUser,
737
863
  user_metadata: {},
@@ -743,12 +869,15 @@ describe('PaceAppLayout Component', () => {
743
869
  user: mockUserWithoutOrg,
744
870
  };
745
871
 
746
- // Mock the useUnifiedAuth hook to return user without org context
747
872
  vi.mocked(useUnifiedAuth).mockReturnValue(mockAuthWithoutOrg);
873
+ mockUseCan.mockReturnValueOnce({
874
+ can: false,
875
+ isLoading: false,
876
+ error: null,
877
+ refetch: vi.fn().mockResolvedValue(undefined),
878
+ });
748
879
 
749
- const { isPermitted } = await import('../../rbac/api');
750
- vi.mocked(isPermitted).mockResolvedValue(false);
751
-
880
+ // Act
752
881
  renderWithProviders(
753
882
  <TestWrapper>
754
883
  <PaceAppLayout
@@ -758,9 +887,10 @@ describe('PaceAppLayout Component', () => {
758
887
  </TestWrapper>
759
888
  );
760
889
 
890
+ // Assert - Component should show access denied
761
891
  await waitFor(() => {
762
- expect(screen.getByText('Access Denied')).toBeInTheDocument();
763
- }, { timeout: 3000 });
892
+ expect(screen.getByRole('heading', { name: 'Access Denied' })).toBeInTheDocument();
893
+ });
764
894
  });
765
895
  });
766
896