@jmruthers/pace-core 0.5.193 → 0.6.1

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 (191) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +7 -1
  3. package/cursor-rules/00-pace-core-compliance.mdc +372 -0
  4. package/cursor-rules/01-standards-compliance.mdc +275 -0
  5. package/cursor-rules/02-project-structure.mdc +200 -0
  6. package/cursor-rules/03-solid-principles.mdc +341 -0
  7. package/cursor-rules/04-testing-standards.mdc +315 -0
  8. package/cursor-rules/05-bug-reports-and-features.mdc +246 -0
  9. package/cursor-rules/06-code-quality.mdc +392 -0
  10. package/cursor-rules/07-tech-stack-compliance.mdc +309 -0
  11. package/cursor-rules/CHANGELOG.md +101 -0
  12. package/cursor-rules/README.md +191 -0
  13. package/dist/{DataTable-Be6dH_dR.d.ts → DataTable-CH1U5Tpy.d.ts} +1 -1
  14. package/dist/{DataTable-5FU7IESH.js → DataTable-DQ7RSOHE.js} +6 -6
  15. package/dist/{PublicPageProvider-C0Sm_e5k.d.ts → PublicPageProvider-ce4xlHYA.d.ts} +34 -155
  16. package/dist/{UnifiedAuthProvider-RGJTDE2C.js → UnifiedAuthProvider-ATAP5UTR.js} +2 -2
  17. package/dist/{chunk-6C4YBBJM 5.js → chunk-3QRJFVBR.js} +1 -1
  18. package/dist/chunk-3QRJFVBR.js.map +1 -0
  19. package/dist/{chunk-IIELH4DL.js → chunk-3XTALGJF.js} +2 -2
  20. package/dist/{chunk-IIELH4DL.js.map → chunk-3XTALGJF.js.map} +1 -1
  21. package/dist/{chunk-HWIIPPNI.js → chunk-4N5C5XZU.js} +20 -20
  22. package/dist/chunk-4N5C5XZU.js.map +1 -0
  23. package/dist/{chunk-7EQTDTTJ.js → chunk-4ZC4GX36.js} +5 -5
  24. package/dist/{chunk-7EQTDTTJ.js 2.map → chunk-4ZC4GX36.js.map} +1 -1
  25. package/dist/{chunk-7FLMSG37.js → chunk-BYFSK72L.js} +22 -22
  26. package/dist/chunk-BYFSK72L.js.map +1 -0
  27. package/dist/{chunk-LFNCN2SP.js → chunk-EXUD6RNJ.js} +46 -7
  28. package/dist/chunk-EXUD6RNJ.js.map +1 -0
  29. package/dist/{chunk-NOAYCWCX 5.js → chunk-GLK6VM3F.js} +167 -169
  30. package/dist/chunk-GLK6VM3F.js.map +1 -0
  31. package/dist/{chunk-HW3OVDUF.js → chunk-J36DSWQK.js} +1 -1
  32. package/dist/{chunk-HW3OVDUF.js.map → chunk-J36DSWQK.js.map} +1 -1
  33. package/dist/{chunk-BC4IJKSL.js → chunk-JBKQ3SAO.js} +2 -2
  34. package/dist/{chunk-QWWZ5CAQ.js → chunk-LXQLPRQ2.js} +2 -2
  35. package/dist/{chunk-E3SPN4VZ 5.js → chunk-T33XF5ZC.js} +119 -114
  36. package/dist/chunk-T33XF5ZC.js.map +1 -0
  37. package/dist/{chunk-XNXXZ43G.js → chunk-XM25TVIE.js} +27 -4
  38. package/dist/chunk-XM25TVIE.js.map +1 -0
  39. package/dist/components.d.ts +3 -3
  40. package/dist/components.js +8 -8
  41. package/dist/hooks.d.ts +6 -6
  42. package/dist/hooks.js +17 -22
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/index.d.ts +7 -7
  45. package/dist/index.js +15 -16
  46. package/dist/index.js.map +1 -1
  47. package/dist/providers.js +1 -1
  48. package/dist/rbac/index.d.ts +1 -1
  49. package/dist/rbac/index.js +5 -5
  50. package/dist/{usePublicRouteParams-TZe0gy-4.d.ts → usePublicRouteParams-BJAlWfuJ.d.ts} +3 -3
  51. package/dist/{useToast-C8gR5ir4.d.ts → useToast-AyaT-x7p.d.ts} +2 -2
  52. package/dist/utils.d.ts +1 -1
  53. package/dist/utils.js +3 -3
  54. package/docs/getting-started/cursor-rules.md +262 -0
  55. package/docs/getting-started/installation-guide.md +6 -1
  56. package/docs/getting-started/quick-start.md +6 -1
  57. package/docs/migration/MIGRATION_GUIDE.md +4 -4
  58. package/docs/migration/REACT_19_MIGRATION.md +227 -0
  59. package/docs/standards/README.md +39 -0
  60. package/docs/troubleshooting/migration.md +4 -4
  61. package/examples/PublicPages/PublicEventPage.tsx +1 -1
  62. package/package.json +11 -6
  63. package/scripts/audit-consuming-app.cjs +961 -0
  64. package/scripts/check-pace-core-compliance.cjs +34 -15
  65. package/scripts/install-cursor-rules.cjs +236 -0
  66. package/src/__tests__/helpers/test-providers.tsx +1 -1
  67. package/src/__tests__/helpers/test-utils.tsx +1 -1
  68. package/src/components/Badge/Badge.tsx +2 -4
  69. package/src/components/Button/Button.tsx +5 -4
  70. package/src/components/Calendar/Calendar.tsx +1 -1
  71. package/src/components/DataTable/DataTable.test.tsx +57 -93
  72. package/src/components/DataTable/DataTable.tsx +2 -2
  73. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +13 -5
  74. package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +12 -12
  75. package/src/components/DataTable/components/AccessDeniedPage.tsx +1 -1
  76. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +1 -1
  77. package/src/components/DataTable/components/DataTableCore.tsx +4 -7
  78. package/src/components/DataTable/components/DataTableModals.tsx +1 -1
  79. package/src/components/DataTable/components/EditableRow.tsx +1 -1
  80. package/src/components/DataTable/components/UnifiedTableBody.tsx +6 -8
  81. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +23 -23
  82. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +11 -11
  83. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +36 -36
  84. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +27 -27
  85. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +39 -39
  86. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +33 -33
  87. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +29 -29
  88. package/src/components/DataTable/hooks/useColumnReordering.ts +2 -2
  89. package/src/components/DataTable/hooks/useKeyboardNavigation.ts +2 -2
  90. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -14
  91. package/src/components/Dialog/Dialog.tsx +6 -5
  92. package/src/components/ErrorBoundary/ErrorBoundary.tsx +1 -1
  93. package/src/components/EventSelector/EventSelector.tsx +1 -1
  94. package/src/components/FileDisplay/FileDisplay.test.tsx +2 -2
  95. package/src/components/Footer/Footer.tsx +1 -1
  96. package/src/components/Form/Form.test.tsx +36 -15
  97. package/src/components/Form/Form.tsx +30 -26
  98. package/src/components/Header/Header.tsx +1 -1
  99. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +40 -40
  100. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +1 -1
  101. package/src/components/Input/Input.tsx +28 -30
  102. package/src/components/Label/Label.tsx +1 -1
  103. package/src/components/LoadingSpinner/LoadingSpinner.tsx +1 -1
  104. package/src/components/LoginForm/LoginForm.test.tsx +42 -42
  105. package/src/components/LoginForm/LoginForm.tsx +8 -8
  106. package/src/components/NavigationMenu/NavigationMenu.tsx +1 -1
  107. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +1 -1
  108. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +50 -50
  109. package/src/components/PaceAppLayout/PaceAppLayout.tsx +1 -1
  110. package/src/components/PaceAppLayout/README.md +1 -1
  111. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
  112. package/src/components/PasswordChange/PasswordChangeForm.test.tsx +33 -33
  113. package/src/components/PasswordChange/PasswordChangeForm.tsx +1 -1
  114. package/src/components/Progress/Progress.tsx +1 -1
  115. package/src/components/PublicLayout/PublicPageLayout.tsx +1 -1
  116. package/src/components/Select/Select.tsx +33 -22
  117. package/src/components/SessionRestorationLoader/SessionRestorationLoader.tsx +1 -1
  118. package/src/components/Table/Table.tsx +1 -1
  119. package/src/components/Textarea/Textarea.tsx +27 -29
  120. package/src/components/Toast/Toast.tsx +1 -1
  121. package/src/components/Tooltip/Tooltip.tsx +1 -1
  122. package/src/components/UserMenu/UserMenu.tsx +1 -1
  123. package/src/hooks/__tests__/hooks.integration.test.tsx +80 -55
  124. package/src/hooks/__tests__/useStorage.unit.test.ts +36 -36
  125. package/src/hooks/public/usePublicEvent.ts +1 -1
  126. package/src/hooks/public/usePublicEventLogo.ts +1 -1
  127. package/src/hooks/public/usePublicRouteParams.ts +1 -1
  128. package/src/hooks/useDataTableState.ts +8 -18
  129. package/src/hooks/useFocusManagement.ts +2 -2
  130. package/src/hooks/useFocusTrap.ts +4 -4
  131. package/src/hooks/useFormDialog.ts +8 -7
  132. package/src/hooks/useInactivityTracker.ts +1 -1
  133. package/src/hooks/usePermissionCache.ts +1 -1
  134. package/src/hooks/useSecureDataAccess.ts +19 -4
  135. package/src/hooks/useToast.ts +2 -2
  136. package/src/providers/__tests__/OrganisationProvider.test.tsx +57 -13
  137. package/src/providers/__tests__/ProviderLifecycle.test.tsx +21 -6
  138. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +10 -10
  139. package/src/providers/services/UnifiedAuthProvider.tsx +22 -22
  140. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +13 -3
  141. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +24 -24
  142. package/src/rbac/components/EnhancedNavigationMenu.tsx +1 -1
  143. package/src/rbac/components/NavigationGuard.tsx +1 -1
  144. package/src/rbac/components/NavigationProvider.tsx +1 -1
  145. package/src/rbac/components/PagePermissionGuard.tsx +1 -1
  146. package/src/rbac/components/PagePermissionProvider.tsx +1 -1
  147. package/src/rbac/components/PermissionEnforcer.tsx +1 -1
  148. package/src/rbac/components/RoleBasedRouter.tsx +1 -1
  149. package/src/rbac/components/SecureDataProvider.tsx +1 -1
  150. package/src/rbac/secureClient.ts +12 -0
  151. package/src/utils/security/secureDataAccess.test.ts +31 -20
  152. package/src/utils/security/secureDataAccess.ts +4 -3
  153. package/dist/chunk-6C4YBBJM.js +0 -628
  154. package/dist/chunk-6C4YBBJM.js.map +0 -1
  155. package/dist/chunk-7D4SUZUM.js 2.map +0 -1
  156. package/dist/chunk-7EQTDTTJ.js.map +0 -1
  157. package/dist/chunk-7FLMSG37.js 2.map +0 -1
  158. package/dist/chunk-7FLMSG37.js.map +0 -1
  159. package/dist/chunk-E3SPN4VZ.js +0 -12917
  160. package/dist/chunk-E3SPN4VZ.js.map +0 -1
  161. package/dist/chunk-E66EQZE6 5.js +0 -37
  162. package/dist/chunk-E66EQZE6.js 2.map +0 -1
  163. package/dist/chunk-HWIIPPNI.js.map +0 -1
  164. package/dist/chunk-I7PSE6JW 5.js +0 -191
  165. package/dist/chunk-I7PSE6JW.js 2.map +0 -1
  166. package/dist/chunk-KNC55RTG.js 5.map +0 -1
  167. package/dist/chunk-KQCRWDSA.js 5.map +0 -1
  168. package/dist/chunk-LFNCN2SP.js 2.map +0 -1
  169. package/dist/chunk-LFNCN2SP.js.map +0 -1
  170. package/dist/chunk-LMC26NLJ 2.js +0 -84
  171. package/dist/chunk-NOAYCWCX.js +0 -4993
  172. package/dist/chunk-NOAYCWCX.js.map +0 -1
  173. package/dist/chunk-QWWZ5CAQ.js.map +0 -1
  174. package/dist/chunk-QXHPKYJV 3.js +0 -113
  175. package/dist/chunk-R77UEZ4E 3.js +0 -68
  176. package/dist/chunk-VBXEHIUJ.js 6.map +0 -1
  177. package/dist/chunk-XNXXZ43G.js.map +0 -1
  178. package/dist/chunk-ZSAAAMVR 6.js +0 -25
  179. package/dist/components.js 5.map +0 -1
  180. package/dist/styles/index 2.js +0 -12
  181. package/dist/styles/index.js 5.map +0 -1
  182. package/dist/theming/runtime 5.js +0 -19
  183. package/dist/theming/runtime.js 5.map +0 -1
  184. /package/dist/{DataTable-5FU7IESH.js.map → DataTable-DQ7RSOHE.js.map} +0 -0
  185. /package/dist/{UnifiedAuthProvider-RGJTDE2C.js.map → UnifiedAuthProvider-ATAP5UTR.js.map} +0 -0
  186. /package/dist/{chunk-BC4IJKSL.js.map → chunk-JBKQ3SAO.js.map} +0 -0
  187. /package/dist/{chunk-QWWZ5CAQ.js 3.map → chunk-LXQLPRQ2.js.map} +0 -0
  188. /package/examples/{rbac → RBAC}/CompleteRBACExample.tsx +0 -0
  189. /package/examples/{rbac → RBAC}/EventBasedApp.tsx +0 -0
  190. /package/examples/{rbac → RBAC}/PermissionExample.tsx +0 -0
  191. /package/examples/{rbac → RBAC}/index.ts +0 -0
@@ -237,9 +237,11 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
237
237
  // SECURITY: Phase 2 additions - complete organisation table mapping
238
238
  'organisation_audit_log', 'organisation_invitations', 'organisation_app_access',
239
239
  // SECURITY: Emergency additions for Phase 1 fixes
240
- 'cake_meal', 'cake_mealtype', 'core_person',
241
- // NOTE: core_member, medi_profile, core_contact, core_consent, core_identification, core_qualification
242
- // are now person-scoped (not organisation-scoped) - removed from this list
240
+ 'cake_meal', 'cake_mealtype',
241
+ // NOTE: core_person, core_member, core_contact, core_consent, core_identification,
242
+ // core_qualification, and medi_profile are person-scoped tables that do NOT have
243
+ // organisation_id columns. They were removed as part of the person-scoped profiles migration.
244
+ // Do NOT add organisation_id filters to these tables - it will cause 400 errors.
243
245
  // SECURITY: Phase 3A additions - medical and personal data
244
246
  // NOTE: medi_condition, medi_diet, medi_action_plan, medi_profile_versions are now person-scoped
245
247
  // (via medi_profile) - removed from this list
@@ -370,7 +372,20 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
370
372
  // SECURITY: Phase 2 additions - complete organisation table mapping
371
373
  'organisation_audit_log', 'organisation_invitations', 'organisation_app_access',
372
374
  // SECURITY: Emergency additions for Phase 1 fixes
373
- 'cake_meal', 'cake_mealtype', 'core_person', 'core_member'
375
+ 'cake_meal', 'cake_mealtype', 'core_person',
376
+ // SECURITY: These tables still have organisation_id columns and must be filtered
377
+ 'core_member', 'core_contact', 'core_consent', 'core_identification', 'core_qualification',
378
+ 'medi_profile',
379
+ // SECURITY: Phase 3A additions - medical and personal data
380
+ 'medi_condition', 'medi_diet', 'medi_action_plan', 'medi_profile_versions',
381
+ 'core_identification_type',
382
+ 'form_responses', 'form_response_values', 'forms',
383
+ // SECURITY: Phase 3B additions - remaining critical tables
384
+ 'invoice', 'line_item', 'credit_balance', 'payment_method',
385
+ 'form_contexts', 'form_field_config', 'form_fields',
386
+ 'cake_delivery', 'cake_diettype', 'cake_diner', 'cake_dish', 'cake_item',
387
+ 'cake_logistics', 'cake_mealplan', 'cake_package', 'cake_recipe', 'cake_supplier',
388
+ 'cake_supply', 'cake_unit', 'event_app_access', 'base_application', 'base_questions'
374
389
  ];
375
390
 
376
391
  if (!bypassOrganisationFilter && organisationId && tablesWithOrganisation.includes(table)) {
@@ -23,7 +23,7 @@ export interface ToastProps {
23
23
  description?: React.ReactNode;
24
24
  variant?: 'default' | 'destructive' | 'success';
25
25
  onClose?: () => void;
26
- action?: React.ReactElement;
26
+ action?: React.ReactElement<any>;
27
27
  }
28
28
 
29
29
  /**
@@ -40,7 +40,7 @@ type ToasterToast = ToastProps & {
40
40
  /** Optional description content */
41
41
  description?: React.ReactNode
42
42
  /** Optional action button */
43
- action?: React.ReactElement
43
+ action?: React.ReactElement<any>
44
44
  /** Open state */
45
45
  open?: boolean
46
46
  /** Open change handler */
@@ -17,9 +17,14 @@ vi.mock('../../utils/debugLogger', () => ({
17
17
  },
18
18
  }));
19
19
 
20
- // Mock setOrganisationContext
20
+ // Mock setOrganisationContext - make it resolve immediately to avoid delays
21
21
  vi.mock('../../utils/context/organisationContext', () => ({
22
- setOrganisationContext: vi.fn().mockResolvedValue(undefined),
22
+ setOrganisationContext: vi.fn().mockImplementation(() => Promise.resolve(undefined)),
23
+ }));
24
+
25
+ // Mock isSuperAdmin to return false (normal user, not super admin)
26
+ vi.mock('../../rbac/api', () => ({
27
+ isSuperAdmin: vi.fn().mockResolvedValue(false),
23
28
  }));
24
29
 
25
30
  // Create mock user and session
@@ -63,18 +68,36 @@ const createMockSupabaseClient = () => {
63
68
  role: 'org_admin',
64
69
  status: 'active',
65
70
  granted_at: '2023-01-01T00:00:00Z',
71
+ granted_by: null,
66
72
  revoked_at: null,
67
- core_organisations: mockOrganisation
73
+ revoked_by: null,
74
+ notes: null,
75
+ created_at: '2023-01-01T00:00:00Z',
76
+ updated_at: '2023-01-01T00:00:00Z',
77
+ core_organisations: {
78
+ id: orgId,
79
+ name: 'Test Organisation 1',
80
+ display_name: 'Test Organisation 1',
81
+ subscription_tier: 'basic',
82
+ settings: {},
83
+ is_active: true,
84
+ parent_id: null,
85
+ created_at: '2023-01-01T00:00:00Z',
86
+ updated_at: '2023-01-01T00:00:00Z'
87
+ }
68
88
  }],
69
89
  error: null
70
90
  })
71
91
  };
72
92
 
93
+ // Mock RPC function for setOrganisationContext
94
+ const mockRpc = vi.fn().mockResolvedValue({
95
+ data: null,
96
+ error: null,
97
+ });
98
+
73
99
  return {
74
- rpc: vi.fn().mockResolvedValue({
75
- data: [],
76
- error: null,
77
- }),
100
+ rpc: mockRpc,
78
101
  from: vi.fn((table: string) => {
79
102
  if (table === 'rbac_organisation_roles') {
80
103
  return mockQueryBuilder;
@@ -127,6 +150,8 @@ describe('OrganisationProvider', () => {
127
150
 
128
151
  beforeEach(() => {
129
152
  vi.clearAllMocks();
153
+ // Clear localStorage to ensure clean state
154
+ localStorage.clear();
130
155
  mockSupabaseClient = createMockSupabaseClient();
131
156
  });
132
157
 
@@ -140,10 +165,19 @@ describe('OrganisationProvider', () => {
140
165
 
141
166
  expect(screen.getByTestId('test-component')).toBeInTheDocument();
142
167
 
143
- // Wait for organisations to load
168
+ // Wait for organisations to load - OrganisationService initializes asynchronously
169
+ // The service calls notify() in finally block, and useOrganisationService has 50ms debounce
170
+ // Also need to account for the 2 second minimum retry delay in loadUserOrganisations
144
171
  await waitFor(() => {
145
- expect(screen.getByTestId('organisations-count')).toHaveTextContent('1');
146
- }, { timeout: 3000 });
172
+ const count = screen.getByTestId('organisations-count').textContent;
173
+ const error = screen.getByTestId('error').textContent;
174
+ // Log for debugging if test fails
175
+ if (count !== '1' || error !== 'no-error') {
176
+ console.log('Test state:', { count, error, isLoading: screen.getByTestId('isLoading').textContent });
177
+ }
178
+ expect(count).toBe('1');
179
+ expect(error).toBe('no-error');
180
+ }, { timeout: 15000, interval: 200 });
147
181
 
148
182
  expect(screen.getByTestId('isLoading')).toHaveTextContent('false');
149
183
  expect(screen.getByTestId('error')).toHaveTextContent('no-error');
@@ -169,10 +203,20 @@ describe('OrganisationProvider', () => {
169
203
  );
170
204
 
171
205
  // Wait for organisations to load and selected organisation to be set
206
+ // The service calls notify() in finally block, and useOrganisationService has 50ms debounce
207
+ // Also need to account for the 2 second minimum retry delay in loadUserOrganisations
172
208
  await waitFor(() => {
173
- expect(screen.getByTestId('organisations-count')).toHaveTextContent('1');
174
- expect(screen.getByTestId('selectedOrg')).not.toHaveTextContent('no-org');
175
- }, { timeout: 3000 });
209
+ const count = screen.getByTestId('organisations-count').textContent;
210
+ const error = screen.getByTestId('error').textContent;
211
+ const selectedOrg = screen.getByTestId('selectedOrg').textContent;
212
+ // Log for debugging if test fails
213
+ if (count !== '1' || error !== 'no-error' || selectedOrg === 'no-org') {
214
+ console.log('Test state:', { count, error, selectedOrg, isLoading: screen.getByTestId('isLoading').textContent });
215
+ }
216
+ expect(count).toBe('1');
217
+ expect(error).toBe('no-error');
218
+ expect(selectedOrg).not.toBe('no-org');
219
+ }, { timeout: 15000, interval: 200 });
176
220
 
177
221
  expect(screen.getByTestId('organisations-count')).toHaveTextContent('1');
178
222
  expect(screen.getByTestId('isLoading')).toHaveTextContent('false');
@@ -7,7 +7,7 @@
7
7
  * Tests for provider lifecycle management, state persistence, cleanup, and edge cases.
8
8
  */
9
9
 
10
- import { render, screen, fireEvent } from '@testing-library/react';
10
+ import { render, screen, fireEvent, act } from '@testing-library/react';
11
11
  import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
12
12
  import React, { ReactNode, useState } from 'react';
13
13
  import { createMockSupabaseClient } from '../../__tests__/helpers/supabaseMock';
@@ -88,8 +88,12 @@ describe('Provider Lifecycle Tests', () => {
88
88
  it('should cleanup subscription listeners', () => {
89
89
  const cleanup = vi.fn();
90
90
  const TestComponent = () => {
91
+ const cleanupRef = React.useRef(cleanup);
91
92
  React.useEffect(() => {
92
- return cleanup;
93
+ // Return a function that calls cleanup from ref
94
+ return () => {
95
+ cleanupRef.current();
96
+ };
93
97
  }, []);
94
98
  return <div data-testid="listener-test">Test</div>;
95
99
  };
@@ -98,7 +102,9 @@ describe('Provider Lifecycle Tests', () => {
98
102
 
99
103
  expect(screen.getByTestId('listener-test')).toBeInTheDocument();
100
104
 
101
- unmountComponent();
105
+ act(() => {
106
+ unmountComponent();
107
+ });
102
108
 
103
109
  // Cleanup should be called on unmount
104
110
  expect(cleanup).toHaveBeenCalled();
@@ -109,13 +115,22 @@ describe('Provider Lifecycle Tests', () => {
109
115
  const cleanup2 = vi.fn();
110
116
 
111
117
  const TestComponent = () => {
112
- React.useEffect(() => cleanup1, []);
113
- React.useEffect(() => cleanup2, []);
118
+ const cleanup1Ref = React.useRef(cleanup1);
119
+ const cleanup2Ref = React.useRef(cleanup2);
120
+ React.useEffect(() => {
121
+ return () => cleanup1Ref.current();
122
+ }, []);
123
+ React.useEffect(() => {
124
+ return () => cleanup2Ref.current();
125
+ }, []);
114
126
  return <div data-testid="multi-listener">Test</div>;
115
127
  };
116
128
 
117
129
  const { unmount: unmountComponent } = render(<TestComponent />);
118
- unmountComponent();
130
+
131
+ act(() => {
132
+ unmountComponent();
133
+ });
119
134
 
120
135
  expect(cleanup1).toHaveBeenCalled();
121
136
  expect(cleanup2).toHaveBeenCalled();
@@ -72,7 +72,7 @@ const TestComponent = () => {
72
72
 
73
73
  describe('UnifiedAuthProvider', () => {
74
74
  let mockSupabaseClient: ReturnType<typeof createMockSupabaseClient>;
75
- const defaultProps = {
75
+ const baseProps = {
76
76
  idleTimeoutMs: 30 * 60 * 1000,
77
77
  warnBeforeMs: 60 * 1000,
78
78
  onIdleLogout: vi.fn(),
@@ -86,7 +86,7 @@ describe('UnifiedAuthProvider', () => {
86
86
  describe('Rendering', () => {
87
87
  it('renders without crashing', () => {
88
88
  render(
89
- <UnifiedAuthProvider supabaseClient={mockSupabaseClient} {...defaultProps}>
89
+ <UnifiedAuthProvider supabaseClient={mockSupabaseClient} {...baseProps}>
90
90
  <TestComponent />
91
91
  </UnifiedAuthProvider>
92
92
  );
@@ -96,7 +96,7 @@ describe('UnifiedAuthProvider', () => {
96
96
 
97
97
  it('renders with custom app name', () => {
98
98
  render(
99
- <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Custom App" {...defaultProps}>
99
+ <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Custom App" {...baseProps}>
100
100
  <TestComponent />
101
101
  </UnifiedAuthProvider>
102
102
  );
@@ -118,7 +118,7 @@ describe('UnifiedAuthProvider', () => {
118
118
  describe('Context Values', () => {
119
119
  it('provides all required context values', async () => {
120
120
  render(
121
- <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" {...defaultProps}>
121
+ <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" {...baseProps}>
122
122
  <TestComponent />
123
123
  </UnifiedAuthProvider>
124
124
  );
@@ -182,7 +182,7 @@ describe('UnifiedAuthProvider', () => {
182
182
 
183
183
  it('uses default configuration', () => {
184
184
  render(
185
- <UnifiedAuthProvider supabaseClient={mockSupabaseClient} {...defaultProps}>
185
+ <UnifiedAuthProvider supabaseClient={mockSupabaseClient} {...baseProps}>
186
186
  <TestComponent />
187
187
  </UnifiedAuthProvider>
188
188
  );
@@ -207,7 +207,7 @@ describe('UnifiedAuthProvider', () => {
207
207
  };
208
208
 
209
209
  render(
210
- <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" {...defaultProps}>
210
+ <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" {...baseProps}>
211
211
  <TestCompositionComponent />
212
212
  </UnifiedAuthProvider>
213
213
  );
@@ -236,7 +236,7 @@ describe('UnifiedAuthProvider', () => {
236
236
  };
237
237
 
238
238
  render(
239
- <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" {...defaultProps}>
239
+ <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" {...baseProps}>
240
240
  <TestCompleteContextComponent />
241
241
  </UnifiedAuthProvider>
242
242
  );
@@ -428,7 +428,7 @@ describe('UnifiedAuthProvider', () => {
428
428
  };
429
429
 
430
430
  render(
431
- <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" {...defaultProps}>
431
+ <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" {...baseProps}>
432
432
  <TestHookIntegrationComponent />
433
433
  </UnifiedAuthProvider>
434
434
  );
@@ -494,7 +494,7 @@ describe('UnifiedAuthProvider', () => {
494
494
  };
495
495
 
496
496
  render(
497
- <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" {...defaultProps}>
497
+ <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" {...baseProps}>
498
498
  <TestConcurrentComponent />
499
499
  </UnifiedAuthProvider>
500
500
  );
@@ -554,7 +554,7 @@ describe('UnifiedAuthProvider', () => {
554
554
  };
555
555
 
556
556
  render(
557
- <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" enableRBAC={true} {...defaultProps}>
557
+ <UnifiedAuthProvider supabaseClient={mockSupabaseClient} appName="Test App" enableRBAC={true} {...baseProps}>
558
558
  <TestRBACConfigComponent />
559
559
  </UnifiedAuthProvider>
560
560
  );
@@ -526,31 +526,31 @@ function UnifiedAuthContextProvider({
526
526
 
527
527
  const hasErrors = !!(authError || organisationError || eventError || sessionRestoration.restorationError);
528
528
 
529
- // Create stable references for all methods using useCallback
530
- const signIn = useCallback((email: string, password?: string) => authService.signIn(email, password), [authService]);
531
- const signUp = useCallback((email: string, password: string) => authService.signUp(email, password), [authService]);
532
- const signOut = useCallback(() => authService.signOut(), [authService]);
533
- const resetPassword = useCallback((email: string) => authService.resetPassword(email), [authService]);
534
- const updatePassword = useCallback((password: string) => authService.updatePassword(password), [authService]);
535
- const refreshSession = useCallback(() => authService.refreshSession(), [authService]);
529
+ // React Compiler handles memoization automatically
530
+ const signIn = (email: string, password?: string) => authService.signIn(email, password);
531
+ const signUp = (email: string, password: string) => authService.signUp(email, password);
532
+ const signOut = () => authService.signOut();
533
+ const resetPassword = (email: string) => authService.resetPassword(email);
534
+ const updatePassword = (password: string) => authService.updatePassword(password);
535
+ const refreshSession = () => authService.refreshSession();
536
536
 
537
- const switchOrganisation = useCallback((orgId: string) => organisationService.switchOrganisation(orgId), [organisationService]);
538
- const getUserRole = useCallback((orgId?: string) => organisationService.getUserRole(orgId), [organisationService]);
539
- const validateOrganisationAccess = useCallback((orgId: string) => organisationService.validateOrganisationAccess(orgId), [organisationService]);
540
- const refreshOrganisations = useCallback(() => organisationService.refreshOrganisations(), [organisationService]);
541
- const ensureOrganisationContext = useCallback(() => organisationService.ensureOrganisationContext(), [organisationService]);
542
- const isOrganisationSecure = useCallback(() => organisationService.isOrganisationSecure(), [organisationService]);
543
- const getPrimaryOrganisation = useCallback(() => organisationService.getPrimaryOrganisation(), [organisationService]);
537
+ const switchOrganisation = (orgId: string) => organisationService.switchOrganisation(orgId);
538
+ const getUserRole = (orgId?: string) => organisationService.getUserRole(orgId);
539
+ const validateOrganisationAccess = (orgId: string) => organisationService.validateOrganisationAccess(orgId);
540
+ const refreshOrganisations = () => organisationService.refreshOrganisations();
541
+ const ensureOrganisationContext = () => organisationService.ensureOrganisationContext();
542
+ const isOrganisationSecure = () => organisationService.isOrganisationSecure();
543
+ const getPrimaryOrganisation = () => organisationService.getPrimaryOrganisation();
544
544
 
545
- const setSelectedEvent = useCallback((event: Event | null) => eventService.setSelectedEvent(event), [eventService]);
546
- const refreshEvents = useCallback(() => eventService.refreshEvents(), [eventService]);
545
+ const setSelectedEvent = (event: Event | null) => eventService.setSelectedEvent(event);
546
+ const refreshEvents = () => eventService.refreshEvents();
547
547
 
548
- const resetActivity = useCallback(() => inactivityService.resetActivity(), [inactivityService]);
549
- const startTracking = useCallback(() => inactivityService.startTracking(), [inactivityService]);
550
- const stopTracking = useCallback(() => inactivityService.stopTracking(), [inactivityService]);
551
- const handleIdleLogout = useCallback(() => inactivityService.handleIdleLogout(), [inactivityService]);
552
- const handleStaySignedIn = useCallback(() => inactivityService.handleStaySignedIn(), [inactivityService]);
553
- const handleSignOutNow = useCallback(() => inactivityService.handleSignOutNow(), [inactivityService]);
548
+ const resetActivity = () => inactivityService.resetActivity();
549
+ const startTracking = () => inactivityService.startTracking();
550
+ const stopTracking = () => inactivityService.stopTracking();
551
+ const handleIdleLogout = () => inactivityService.handleIdleLogout();
552
+ const handleStaySignedIn = () => inactivityService.handleStaySignedIn();
553
+ const handleSignOutNow = () => inactivityService.handleSignOutNow();
554
554
 
555
555
  // Use ref to track previous state for conditional logging (dev only)
556
556
  const prevStateRef = useRef<{
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
12
- import { render, screen, waitFor } from '@testing-library/react';
12
+ import { render, screen, waitFor, act } from '@testing-library/react';
13
13
  import React from 'react';
14
14
  import { AuthServiceProvider } from '../AuthServiceProvider';
15
15
  import { useAuthService } from '../../../hooks/services/useAuthService';
@@ -195,16 +195,26 @@ describe('AuthServiceProvider Integration', () => {
195
195
  }, { interval: 10 });
196
196
 
197
197
  // Simulate auth state change using the captured callback
198
+ // Supabase auth state change callback receives (event, session) as arguments
198
199
  if (capturedCallback) {
200
+ // Call the callback - AuthService will update state and notify subscribers
201
+ // The callback is synchronous, but notify() triggers subscribers which have debounce
199
202
  capturedCallback('SIGNED_IN', mockSession);
203
+
204
+ // Wait for debounced subscriber updates (50ms debounce in useAuthService)
205
+ // The test component subscribes directly, so we need to wait for the debounce
206
+ await act(async () => {
207
+ await new Promise(resolve => setTimeout(resolve, 150));
208
+ });
200
209
  }
201
210
 
202
- // Wait for UI updates after state change
211
+ // Wait for UI updates after state change - need longer timeout for async updates
212
+ // The debounce in useAuthService adds a 50ms delay, so we need to wait longer
203
213
  await waitFor(() => {
204
214
  expect(screen.getByTestId('user-id')).toHaveTextContent('test-user-id');
205
215
  expect(screen.getByTestId('is-authenticated')).toHaveTextContent('true');
206
216
  expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
207
- }, { interval: 10 });
217
+ }, { interval: 50, timeout: 10000 });
208
218
  });
209
219
  });
210
220
 
@@ -91,7 +91,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
91
91
  });
92
92
 
93
93
  describe('PermissionGuard Component', () => {
94
- const defaultProps = {
94
+ const baseProps = {
95
95
  userId: 'user-123' as UUID,
96
96
  scope: { organisationId: 'org-123' as UUID },
97
97
  permission: 'read:users' as Permission,
@@ -106,7 +106,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
106
106
  error: null,
107
107
  });
108
108
 
109
- render(<PermissionGuard {...defaultProps} />);
109
+ render(<PermissionGuard {...baseProps} />);
110
110
 
111
111
  expect(screen.getByText('Protected Content')).toBeInTheDocument();
112
112
  });
@@ -119,7 +119,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
119
119
  });
120
120
 
121
121
  const fallback = <div>Access Denied</div>;
122
- render(<PermissionGuard {...defaultProps} fallback={fallback} />);
122
+ render(<PermissionGuard {...baseProps} fallback={fallback} />);
123
123
 
124
124
  expect(screen.getByText('Access Denied')).toBeInTheDocument();
125
125
  expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
@@ -133,7 +133,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
133
133
  });
134
134
 
135
135
  const loading = <div>Checking permissions...</div>;
136
- render(<PermissionGuard {...defaultProps} loading={loading} />);
136
+ render(<PermissionGuard {...baseProps} loading={loading} />);
137
137
 
138
138
  expect(screen.getByText('Checking permissions...')).toBeInTheDocument();
139
139
  expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
@@ -146,7 +146,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
146
146
  error: null,
147
147
  });
148
148
 
149
- render(<PermissionGuard {...defaultProps} />);
149
+ render(<PermissionGuard {...baseProps} />);
150
150
 
151
151
  expect(screen.getByRole('status')).toBeInTheDocument();
152
152
  expect(screen.getByText('Checking permissions...')).toBeInTheDocument();
@@ -160,7 +160,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
160
160
  });
161
161
 
162
162
  const fallback = <div>Error occurred</div>;
163
- render(<PermissionGuard {...defaultProps} fallback={fallback} />);
163
+ render(<PermissionGuard {...baseProps} fallback={fallback} />);
164
164
 
165
165
  expect(screen.getByText('Error occurred')).toBeInTheDocument();
166
166
  expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
@@ -180,7 +180,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
180
180
 
181
181
  render(
182
182
  <PermissionGuard
183
- {...defaultProps}
183
+ {...baseProps}
184
184
  userId={'' as any}
185
185
  fallback={<div>No Access</div>}
186
186
  />
@@ -202,7 +202,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
202
202
 
203
203
  render(
204
204
  <PermissionGuard
205
- {...defaultProps}
205
+ {...baseProps}
206
206
  userId={'' as any}
207
207
  fallback={<div>No Access</div>}
208
208
  />
@@ -221,7 +221,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
221
221
  error: null,
222
222
  });
223
223
 
224
- render(<PermissionGuard {...defaultProps} onDenied={onDenied} />);
224
+ render(<PermissionGuard {...baseProps} onDenied={onDenied} />);
225
225
 
226
226
  expect(onDenied).toHaveBeenCalledTimes(1);
227
227
  });
@@ -234,7 +234,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
234
234
  error: null,
235
235
  });
236
236
 
237
- render(<PermissionGuard {...defaultProps} onDenied={onDenied} />);
237
+ render(<PermissionGuard {...baseProps} onDenied={onDenied} />);
238
238
 
239
239
  expect(onDenied).not.toHaveBeenCalled();
240
240
  });
@@ -255,7 +255,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
255
255
  error: null,
256
256
  });
257
257
 
258
- render(<PermissionGuard {...defaultProps} auditLog={true} />);
258
+ render(<PermissionGuard {...baseProps} auditLog={true} />);
259
259
 
260
260
  // Note: Audit logging is currently commented out in PermissionGuard
261
261
  // The component checks auditLog but doesn't actually log - this is expected behavior
@@ -278,7 +278,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
278
278
  error: null,
279
279
  });
280
280
 
281
- render(<PermissionGuard {...defaultProps} auditLog={true} />);
281
+ render(<PermissionGuard {...baseProps} auditLog={true} />);
282
282
 
283
283
  // Note: Audit logging is currently commented out in PermissionGuard
284
284
  // The component checks auditLog but doesn't actually log - this is expected behavior
@@ -301,7 +301,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
301
301
  error: null,
302
302
  });
303
303
 
304
- render(<PermissionGuard {...defaultProps} strictMode={true} />);
304
+ render(<PermissionGuard {...baseProps} strictMode={true} />);
305
305
 
306
306
  expect(mockLoggerInstance.error).toHaveBeenCalledWith(
307
307
  expect.stringContaining('STRICT MODE VIOLATION:'),
@@ -316,7 +316,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
316
316
  });
317
317
 
318
318
  describe('AccessLevelGuard Component', () => {
319
- const defaultProps = {
319
+ const baseProps = {
320
320
  userId: 'user-123' as UUID,
321
321
  scope: { organisationId: 'org-123' as UUID },
322
322
  minLevel: 'admin' as const,
@@ -331,7 +331,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
331
331
  error: null,
332
332
  });
333
333
 
334
- render(<AccessLevelGuard {...defaultProps} />);
334
+ render(<AccessLevelGuard {...baseProps} />);
335
335
 
336
336
  expect(screen.getByText('Admin Content')).toBeInTheDocument();
337
337
  });
@@ -344,7 +344,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
344
344
  });
345
345
 
346
346
  const fallback = <div>Insufficient Access</div>;
347
- render(<AccessLevelGuard {...defaultProps} fallback={fallback} />);
347
+ render(<AccessLevelGuard {...baseProps} fallback={fallback} />);
348
348
 
349
349
  expect(screen.getByText('Insufficient Access')).toBeInTheDocument();
350
350
  expect(screen.queryByText('Admin Content')).not.toBeInTheDocument();
@@ -358,7 +358,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
358
358
  });
359
359
 
360
360
  const loading = <div>Checking access level...</div>;
361
- render(<AccessLevelGuard {...defaultProps} loading={loading} />);
361
+ render(<AccessLevelGuard {...baseProps} loading={loading} />);
362
362
 
363
363
  expect(screen.getByText('Checking access level...')).toBeInTheDocument();
364
364
  expect(screen.queryByText('Admin Content')).not.toBeInTheDocument();
@@ -372,7 +372,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
372
372
  });
373
373
 
374
374
  const fallback = <div>Error occurred</div>;
375
- render(<AccessLevelGuard {...defaultProps} fallback={fallback} />);
375
+ render(<AccessLevelGuard {...baseProps} fallback={fallback} />);
376
376
 
377
377
  expect(screen.getByText('Error occurred')).toBeInTheDocument();
378
378
  expect(screen.queryByText('Admin Content')).not.toBeInTheDocument();
@@ -387,7 +387,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
387
387
  error: null,
388
388
  });
389
389
 
390
- render(<AccessLevelGuard {...defaultProps} minLevel="admin" />);
390
+ render(<AccessLevelGuard {...baseProps} minLevel="admin" />);
391
391
 
392
392
  expect(screen.getByText('Admin Content')).toBeInTheDocument();
393
393
  });
@@ -399,7 +399,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
399
399
  error: null,
400
400
  });
401
401
 
402
- render(<AccessLevelGuard {...defaultProps} minLevel="admin" />);
402
+ render(<AccessLevelGuard {...baseProps} minLevel="admin" />);
403
403
 
404
404
  expect(screen.getByText('Admin Content')).toBeInTheDocument();
405
405
  });
@@ -411,7 +411,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
411
411
  error: null,
412
412
  });
413
413
 
414
- render(<AccessLevelGuard {...defaultProps} minLevel="admin" />);
414
+ render(<AccessLevelGuard {...baseProps} minLevel="admin" />);
415
415
 
416
416
  expect(screen.queryByText('Admin Content')).not.toBeInTheDocument();
417
417
  });
@@ -423,7 +423,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
423
423
  error: null,
424
424
  });
425
425
 
426
- render(<AccessLevelGuard {...defaultProps} minLevel="planner" />);
426
+ render(<AccessLevelGuard {...baseProps} minLevel="planner" />);
427
427
 
428
428
  expect(screen.getByText('Admin Content')).toBeInTheDocument();
429
429
  });
@@ -439,7 +439,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
439
439
 
440
440
  render(
441
441
  <AccessLevelGuard
442
- {...defaultProps}
442
+ {...baseProps}
443
443
  userId={'' as any}
444
444
  minLevel="admin"
445
445
  fallback={<div>No Access</div>}
@@ -462,7 +462,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
462
462
 
463
463
  render(
464
464
  <AccessLevelGuard
465
- {...defaultProps}
465
+ {...baseProps}
466
466
  userId={'' as any}
467
467
  minLevel="admin"
468
468
  fallback={<div>No Access</div>}
@@ -49,7 +49,7 @@
49
49
  * - Efficient filtering
50
50
  *
51
51
  * @dependencies
52
- * - React 18+ - Component framework
52
+ * - React 19+ - Component framework
53
53
  * - NavigationProvider - Navigation permission context
54
54
  * - NavigationGuard - Individual navigation item protection
55
55
  * - RBAC types - Type definitions
@@ -58,7 +58,7 @@
58
58
  * - Efficient error handling
59
59
  *
60
60
  * @dependencies
61
- * - React 18+ - Component framework
61
+ * - React 19+ - Component framework
62
62
  * - useCan hook - Permission checking
63
63
  * - useUnifiedAuth - Authentication context
64
64
  * - RBAC types - Type definitions
@@ -49,7 +49,7 @@
49
49
  * - Cached permission checks
50
50
  *
51
51
  * @dependencies
52
- * - React 18+ - Context and hooks
52
+ * - React 19+ - Context and hooks
53
53
  * - useUnifiedAuth - Authentication context
54
54
  * - RBAC types - Type definitions
55
55
  */