@jmruthers/pace-core 0.5.87 → 0.5.88

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 (242) hide show
  1. package/dist/{AuthService-Df3IozMG.d.ts → AuthService-DcTI5Ov4.d.ts} +9 -0
  2. package/dist/{DataTable-FA6EUX5M.js → DataTable-PWBMKMOG.js} +7 -7
  3. package/dist/{PublicLoadingSpinner-DecuJBX0.d.ts → PublicLoadingSpinner-BQXD1fbO.d.ts} +160 -130
  4. package/dist/{UnifiedAuthProvider-K2IZAY5F.js → UnifiedAuthProvider-5D3HEQND.js} +4 -4
  5. package/dist/{UnifiedAuthProvider-B391Aqum.d.ts → UnifiedAuthProvider-BVKmQd9u.d.ts} +4 -0
  6. package/dist/auth-DReDSLq9.d.ts +16 -0
  7. package/dist/{chunk-CBSD3BZ3.js → chunk-3RZBKQ5Y.js} +2 -6
  8. package/dist/{chunk-CBSD3BZ3.js.map → chunk-3RZBKQ5Y.js.map} +1 -1
  9. package/dist/{chunk-NTW3KGS4.js → chunk-6UHXQH7P.js} +5 -5
  10. package/dist/{chunk-YVUZWLQG.js → chunk-AQGF5OG7.js} +3 -3
  11. package/dist/{chunk-CVMVPYAL.js → chunk-BDZUMRBD.js} +3 -5
  12. package/dist/chunk-BDZUMRBD.js.map +1 -0
  13. package/dist/{chunk-KAY3K5TP.js → chunk-BNXBJOGL.js} +4 -4
  14. package/dist/{chunk-I7O3RSMN.js → chunk-CJIZS3UE.js} +1298 -769
  15. package/dist/chunk-CJIZS3UE.js.map +1 -0
  16. package/dist/{chunk-S3JKDMD5.js → chunk-CXKMRKRF.js} +4 -4
  17. package/dist/{chunk-5BN3YGNK.js → chunk-DP5X5ORK.js} +217 -27
  18. package/dist/chunk-DP5X5ORK.js.map +1 -0
  19. package/dist/{chunk-ZFLOV3OM.js → chunk-H3P2RGKZ.js} +352 -16
  20. package/dist/chunk-H3P2RGKZ.js.map +1 -0
  21. package/dist/{chunk-RIXPZJUB.js → chunk-KTPG5VCH.js} +2 -2
  22. package/dist/{chunk-WUXCWRL6.js → chunk-XJ2HZOBU.js} +6 -1
  23. package/dist/chunk-XJ2HZOBU.js.map +1 -0
  24. package/dist/{chunk-2FQEQUJT.js → chunk-XXVM53P4.js} +4 -4
  25. package/dist/{chunk-I2VVV5PQ.js → chunk-YY4YYM3E.js} +2 -2
  26. package/dist/components.d.ts +6 -55
  27. package/dist/components.js +24 -205
  28. package/dist/components.js.map +1 -1
  29. package/dist/{file-reference-9xUOnwyt.d.ts → file-reference-C9isKNPn.d.ts} +67 -2
  30. package/dist/hooks.js +9 -8
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/index.d.ts +152 -26
  33. package/dist/index.js +64 -194
  34. package/dist/index.js.map +1 -1
  35. package/dist/providers.d.ts +5 -3
  36. package/dist/providers.js +3 -3
  37. package/dist/rbac/index.js +8 -8
  38. package/dist/types.d.ts +2 -1
  39. package/dist/types.js +3 -3
  40. package/dist/utils.js +2 -2
  41. package/docs/DOCUMENTATION_AUDIT.md +6 -6
  42. package/docs/DOCUMENTATION_STANDARD.md +137 -0
  43. package/docs/README.md +1 -1
  44. package/docs/api/classes/ColumnFactory.md +1 -1
  45. package/docs/api/classes/ErrorBoundary.md +1 -1
  46. package/docs/api/classes/InvalidScopeError.md +1 -1
  47. package/docs/api/classes/MissingUserContextError.md +1 -1
  48. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  49. package/docs/api/classes/PermissionDeniedError.md +1 -1
  50. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  51. package/docs/api/classes/RBACAuditManager.md +1 -1
  52. package/docs/api/classes/RBACCache.md +1 -1
  53. package/docs/api/classes/RBACEngine.md +1 -1
  54. package/docs/api/classes/RBACError.md +1 -1
  55. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  56. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  57. package/docs/api/classes/StorageUtils.md +83 -40
  58. package/docs/api/enums/FileCategory.md +56 -1
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  65. package/docs/api/interfaces/DataRecord.md +1 -1
  66. package/docs/api/interfaces/DataTableAction.md +1 -1
  67. package/docs/api/interfaces/DataTableColumn.md +1 -1
  68. package/docs/api/interfaces/DataTableProps.md +1 -1
  69. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  70. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  71. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  72. package/docs/api/interfaces/EventLogoProps.md +11 -11
  73. package/docs/api/interfaces/FileDisplayProps.md +10 -10
  74. package/docs/api/interfaces/FileMetadata.md +1 -1
  75. package/docs/api/interfaces/FileReference.md +1 -1
  76. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  77. package/docs/api/interfaces/FileUploadOptions.md +8 -8
  78. package/docs/api/interfaces/FileUploadProps.md +137 -42
  79. package/docs/api/interfaces/FooterProps.md +1 -1
  80. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  81. package/docs/api/interfaces/InputProps.md +1 -1
  82. package/docs/api/interfaces/LabelProps.md +1 -1
  83. package/docs/api/interfaces/LoginFormProps.md +1 -1
  84. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  85. package/docs/api/interfaces/NavigationContextType.md +1 -1
  86. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  87. package/docs/api/interfaces/NavigationItem.md +1 -1
  88. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  89. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  90. package/docs/api/interfaces/Organisation.md +1 -1
  91. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  92. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  93. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  94. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  95. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  96. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  97. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  98. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  99. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  100. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  101. package/docs/api/interfaces/PaletteData.md +1 -1
  102. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  103. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  104. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  105. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  106. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  107. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  108. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  109. package/docs/api/interfaces/RBACConfig.md +1 -1
  110. package/docs/api/interfaces/RBACLogger.md +1 -1
  111. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  112. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  113. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  114. package/docs/api/interfaces/RouteConfig.md +1 -1
  115. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  116. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  117. package/docs/api/interfaces/StorageConfig.md +1 -1
  118. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  119. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  120. package/docs/api/interfaces/StorageListOptions.md +1 -1
  121. package/docs/api/interfaces/StorageListResult.md +1 -1
  122. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  123. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  124. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  125. package/docs/api/interfaces/StyleImport.md +1 -1
  126. package/docs/api/interfaces/SwitchProps.md +1 -1
  127. package/docs/api/interfaces/ToastActionElement.md +1 -1
  128. package/docs/api/interfaces/ToastProps.md +1 -1
  129. package/docs/api/interfaces/UnifiedAuthContextType.md +83 -50
  130. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  131. package/docs/api/interfaces/UseEventLogoOptions.md +74 -0
  132. package/docs/api/interfaces/UseEventLogoReturn.md +81 -0
  133. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  134. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicEventLogoOptions.md +6 -6
  136. package/docs/api/interfaces/UsePublicEventLogoReturn.md +6 -6
  137. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  138. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  139. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  140. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  141. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  142. package/docs/api/interfaces/UserEventAccess.md +11 -11
  143. package/docs/api/interfaces/UserMenuProps.md +1 -1
  144. package/docs/api/interfaces/UserProfile.md +1 -1
  145. package/docs/api/modules.md +290 -95
  146. package/docs/api-reference/components.md +1 -18
  147. package/docs/api-reference/hooks.md +1 -4
  148. package/docs/best-practices/testing.md +2 -0
  149. package/docs/documentation-index.md +1 -1
  150. package/docs/getting-started/faq.md +1 -1
  151. package/docs/implementation-guides/file-reference-system.md +592 -58
  152. package/docs/implementation-guides/file-upload-storage.md +137 -73
  153. package/docs/rbac/super-admin-guide.md +18 -70
  154. package/docs/testing/README.md +2 -0
  155. package/package.json +1 -1
  156. package/src/__tests__/TEST_STANDARD.md +674 -0
  157. package/src/__tests__/helpers/test-utils.tsx +3 -2
  158. package/src/components/DataTable/__tests__/{DataTable.comprehensive.test.tsx.skip → DataTable.comprehensive.test.tsx} +17 -18
  159. package/src/components/DataTable/__tests__/{DataTable.test.tsx.skip → DataTable.test.tsx} +14 -22
  160. package/src/components/DataTable/__tests__/{ssr.strict-mode.test.tsx.skip → ssr.strict-mode.test.tsx} +42 -47
  161. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +1 -1
  162. package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +13 -4
  163. package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +1 -1
  164. package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +10 -6
  165. package/src/components/FileDisplay/FileDisplay.test.tsx +257 -0
  166. package/src/components/{FileDisplay.tsx → FileDisplay/FileDisplay.tsx} +111 -10
  167. package/src/components/FileDisplay/index.tsx +4 -0
  168. package/src/components/FileUpload/FileUpload.test.tsx +171 -621
  169. package/src/components/FileUpload/FileUpload.tsx +512 -168
  170. package/src/components/FileUpload/index.tsx +4 -0
  171. package/src/components/Progress/Progress.test.tsx +38 -0
  172. package/src/components/PublicLayout/EventLogo.tsx +6 -4
  173. package/src/components/Select/Select.test.tsx +1 -1
  174. package/src/components/SessionRestorationLoader.tsx +48 -0
  175. package/src/components/Toast/Toast.tsx +13 -8
  176. package/src/components/index.ts +16 -16
  177. package/src/hooks/__tests__/ServiceHooks.test.tsx +615 -0
  178. package/src/hooks/public/usePublicEventLogo.ts +16 -20
  179. package/src/hooks/useEventLogo.ts +316 -0
  180. package/src/hooks/useEvents.ts +0 -5
  181. package/src/hooks/useFileReference.test.ts +659 -0
  182. package/src/hooks/useFileReference.ts +207 -3
  183. package/src/hooks/useSessionRestoration.ts +64 -0
  184. package/src/index.ts +17 -5
  185. package/src/providers/{UnifiedAuthProvider.test.simple.tsx → UnifiedAuthProvider.smoke.test.tsx} +81 -60
  186. package/src/providers/services/AuthServiceProvider.tsx +27 -3
  187. package/src/providers/services/UnifiedAuthProvider.tsx +34 -5
  188. package/src/rbac/{engine.test.simple.ts → RBACEngine.smoke.test.ts} +17 -12
  189. package/src/services/AuthService.ts +142 -20
  190. package/src/services/EventService.ts +0 -4
  191. package/src/types/auth.ts +15 -0
  192. package/src/types/file-reference.ts +73 -1
  193. package/src/types/index.ts +1 -0
  194. package/src/utils/__tests__/organisationContext.unit.test.ts +2 -4
  195. package/src/utils/appNameResolver.simple.test.ts +99 -29
  196. package/src/utils/file-reference.test.ts +535 -0
  197. package/src/utils/file-reference.ts +200 -30
  198. package/src/utils/organisationContext.test.ts +5 -19
  199. package/src/utils/organisationContext.ts +3 -5
  200. package/src/utils/storage/README.md +269 -262
  201. package/src/utils/storage/config.ts +9 -0
  202. package/src/utils/storage/helpers.test.ts +631 -0
  203. package/src/utils/storage/helpers.ts +112 -14
  204. package/src/utils/storage/index.ts +3 -0
  205. package/src/validation/__tests__/sanitization.unit.test.ts +1 -1
  206. package/src/validation/__tests__/schemaUtils.unit.test.ts +1 -1
  207. package/src/validation/__tests__/user.unit.test.ts +1 -1
  208. package/dist/chunk-5BN3YGNK.js.map +0 -1
  209. package/dist/chunk-CVMVPYAL.js.map +0 -1
  210. package/dist/chunk-I7O3RSMN.js.map +0 -1
  211. package/dist/chunk-WUXCWRL6.js.map +0 -1
  212. package/dist/chunk-ZFLOV3OM.js.map +0 -1
  213. package/docs/CONTENT_AUDIT_REPORT.md +0 -253
  214. package/docs/STYLE_GUIDE.md +0 -37
  215. package/examples/RBAC/__tests__/PermissionExample.test.tsx +0 -150
  216. package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +0 -159
  217. package/src/__tests__/TEST_GUIDE_CURSOR.md +0 -1605
  218. package/src/__tests__/TEST_GUIDE_HUMAN.md +0 -103
  219. package/src/components/FileUpload/FileUpload.example.tsx +0 -218
  220. package/src/components/FileUpload/index.ts +0 -6
  221. package/src/components/FileUpload.tsx +0 -176
  222. package/src/components/Progress/index.ts +0 -3
  223. package/src/components/PublicLayout/__tests__/EventLogo.test.tsx +0 -666
  224. package/src/components/SuperAdminGuard.tsx +0 -116
  225. package/src/components/__tests__/FileDisplay.test.tsx +0 -575
  226. package/src/components/__tests__/FileUpload.test.tsx +0 -446
  227. package/src/components/__tests__/SuperAdminGuard.test.tsx +0 -627
  228. package/src/components/examples/PermissionExample.tsx +0 -173
  229. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +0 -583
  230. package/src/hooks/__tests__/usePublicEventLogo.unit.test.ts +0 -640
  231. package/src/types/__tests__/file-reference.test.ts +0 -447
  232. package/src/utils/__tests__/file-reference.test.ts +0 -383
  233. /package/dist/{DataTable-FA6EUX5M.js.map → DataTable-PWBMKMOG.js.map} +0 -0
  234. /package/dist/{UnifiedAuthProvider-K2IZAY5F.js.map → UnifiedAuthProvider-5D3HEQND.js.map} +0 -0
  235. /package/dist/{chunk-NTW3KGS4.js.map → chunk-6UHXQH7P.js.map} +0 -0
  236. /package/dist/{chunk-YVUZWLQG.js.map → chunk-AQGF5OG7.js.map} +0 -0
  237. /package/dist/{chunk-KAY3K5TP.js.map → chunk-BNXBJOGL.js.map} +0 -0
  238. /package/dist/{chunk-S3JKDMD5.js.map → chunk-CXKMRKRF.js.map} +0 -0
  239. /package/dist/{chunk-RIXPZJUB.js.map → chunk-KTPG5VCH.js.map} +0 -0
  240. /package/dist/{chunk-2FQEQUJT.js.map → chunk-XXVM53P4.js.map} +0 -0
  241. /package/dist/{chunk-I2VVV5PQ.js.map → chunk-YY4YYM3E.js.map} +0 -0
  242. /package/src/providers/{OrganisationProvider.test.simple.tsx → OrganisationProvider.context.test.tsx} +0 -0
@@ -1,1605 +0,0 @@
1
- # 🧪 Testing Guidelines & Best Practices
2
-
3
- ## 📋 Table of Contents
4
- - [Testing Philosophy](#testing-philosophy)
5
- - [Test Structure](#test-structure)
6
- - [Naming Conventions](#naming-conventions)
7
- - [Test Categories](#test-categories)
8
- - [Best Practices](#best-practices)
9
- - [Common Patterns](#common-patterns)
10
- - [Anti-Patterns](#anti-patterns)
11
- - [Code Coverage](#code-coverage)
12
- - [Coverage Thresholds](#coverage-thresholds)
13
- - [RBAC Testing Patterns](#rbac-testing-patterns)
14
- - [Service Testing Patterns](#service-testing-patterns)
15
- - [DataTable Testing Patterns](#datatable-testing-patterns)
16
- - [PublicLayout Testing Patterns](#publiclayout-testing-patterns)
17
- - [Service Hook Testing Patterns](#service-hook-testing-patterns)
18
-
19
- ## 🎯 Testing Philosophy
20
-
21
- ### The Testing Pyramid
22
- ```
23
- /\
24
- / \ E2E Tests (Few)
25
- /____\
26
- / \ Integration Tests (Some)
27
- /________\
28
- Unit Tests (Many)
29
- ```
30
-
31
- ### Test Principles
32
- 1. **Fast** - Tests should run quickly
33
- 2. **Independent** - Tests should not depend on each other
34
- 3. **Repeatable** - Tests should produce the same results every time
35
- 4. **Self-Validating** - Tests should have a clear pass/fail result
36
- 5. **Timely** - Tests should be written close to the code they test
37
-
38
- ## 🏗️ Test Structure
39
-
40
- ### File Organization
41
-
42
- **Preferred Structure (Colocated with Source)**:
43
- ```
44
- src/
45
- ├── components/
46
- │ ├── Button/
47
- │ │ ├── Button.tsx
48
- │ │ └── Button.test.tsx ✅ Colocated
49
- │ └── DataTable/
50
- │ ├── DataTable.tsx
51
- │ ├── DataTable.test.tsx ✅ Colocated
52
- │ └── __tests__/ ✅ For integration tests
53
- │ └── DataTable.integration.test.tsx
54
- ├── hooks/
55
- │ ├── useCounter.ts
56
- │ ├── useCounter.test.ts ✅ Colocated
57
- │ └── useDebounce.ts
58
- │ └── useDebounce.test.ts ✅ Colocated
59
- ├── providers/
60
- │ ├── EventProvider.tsx
61
- │ └── __tests__/
62
- │ └── EventProvider.test.tsx ✅ Grouped in __tests__
63
- └── validation/
64
- ├── sanitization.ts
65
- ├── sanitization.unit.test.ts ✅ Colocated
66
- └── __tests__/ ✅ Shared test utilities
67
- ├── sanitization.unit.test.ts
68
- └── common.unit.test.ts
69
- ```
70
-
71
- **When to Use `__tests__/` Subdirectory**:
72
- - Group multiple test files for one source file (e.g., `.test.ts`, `.integration.test.tsx`)
73
- - Shared test utilities within a module
74
- - Integration tests that span components
75
-
76
- **Central `src/__tests__/` Directory** - ONLY for:
77
- - Cross-module integration tests
78
- - Shared test utilities and fixtures
79
- - Test configuration files
80
-
81
- ### Test Colocation Rules
82
-
83
- #### ✅ DO: Colocate Tests with Source
84
- ```typescript
85
- // Component file structure
86
- src/components/Button/
87
- ├── Button.tsx
88
- └── Button.test.tsx ✅ Next to source
89
-
90
- // Hook file structure
91
- src/hooks/
92
- ├── useCounter.ts
93
- └── useCounter.test.ts ✅ Next to source
94
- ```
95
-
96
- #### ⚠️ MAY: Use `__tests__/` Subdirectory
97
- ```typescript
98
- // When you have multiple test files for one component
99
- src/components/DataTable/
100
- ├── DataTable.tsx
101
- └── __tests__/
102
- ├── DataTable.test.tsx ✅ Unit tests
103
- └── DataTable.integration.test.tsx ✅ Integration tests
104
- ```
105
-
106
- #### ❌ DON'T: Duplicate Test Files
107
- ```typescript
108
- // WRONG: Don't create duplicates
109
- src/providers/
110
- ├── OrganisationProvider.tsx
111
- ├── OrganisationProvider.test.tsx ❌ Duplicate!
112
- └── __tests__/
113
- └── OrganisationProvider.test.tsx ❌ Duplicate!
114
-
115
- // RIGHT: Single location
116
- src/providers/
117
- ├── OrganisationProvider.tsx
118
- └── __tests__/
119
- └── OrganisationProvider.test.tsx ✅ One location only
120
- ```
121
-
122
- #### ❌ DON'T: Use Central `__tests__/` for Single-Module Tests
123
- ```typescript
124
- // WRONG: Don't put component tests in central directory
125
- src/__tests__/components/Button.test.tsx ❌ Too far from source
126
-
127
- // RIGHT: Colocate with source
128
- src/components/Button/Button.test.tsx ✅ Near source
129
- ```
130
-
131
- ### Test File Naming
132
- - `ComponentName.test.tsx` - Component tests
133
- - `hookName.test.ts` - Hook tests
134
- - `utilityName.test.ts` - Utility tests
135
- - `feature.integration.test.tsx` - Integration tests
136
-
137
- ## 📝 Naming Conventions
138
-
139
- ### Describe Blocks
140
- ```typescript
141
- describe('ComponentName', () => {
142
- describe('Rendering', () => {
143
- // Rendering tests
144
- });
145
-
146
- describe('Event Handling', () => {
147
- // Event tests
148
- });
149
-
150
- describe('State Management', () => {
151
- // State tests
152
- });
153
- });
154
- ```
155
-
156
- ### Test Names
157
- ```typescript
158
- // ✅ Good
159
- it('renders with text content', () => {});
160
- it('handles click events', () => {});
161
- it('can be disabled', () => {});
162
-
163
- // ❌ Bad
164
- it('works', () => {});
165
- it('test button', () => {});
166
- it('should do something', () => {});
167
- ```
168
-
169
- ## 🎭 Test Categories
170
-
171
- ### 1. Unit Tests
172
- - Test individual functions/components in isolation
173
- - Fast, focused, and numerous
174
- - Mock all dependencies
175
-
176
- ### 2. Integration Tests
177
- - Test how multiple units work together
178
- - Test data flow between components
179
- - Use real implementations where possible
180
-
181
- ### 3. Component Tests
182
- - Test React components in isolation
183
- - Test rendering, props, events, and state
184
- - Use React Testing Library
185
-
186
- ### 4. Hook Tests
187
- - Test custom hooks in isolation
188
- - Use `renderHook` from React Testing Library
189
- - Test state changes and side effects
190
-
191
- ## ✅ Best Practices
192
-
193
- ### 1. Test Structure (AAA Pattern)
194
- ```typescript
195
- it('should increment counter when clicked', () => {
196
- // Arrange
197
- const { getByRole } = render(<Counter />);
198
- const button = getByRole('button');
199
-
200
- // Act
201
- fireEvent.click(button);
202
-
203
- // Assert
204
- expect(button).toHaveTextContent('1');
205
- });
206
- ```
207
-
208
- ### 2. Use Semantic Queries
209
- ```typescript
210
- // ✅ Good - Semantic queries
211
- screen.getByRole('button', { name: 'Submit' });
212
- screen.getByLabelText('Email address');
213
- screen.getByText('Welcome back');
214
-
215
- // ❌ Bad - Implementation details
216
- screen.getByClassName('btn-primary');
217
- screen.getByTestId('submit-button');
218
- ```
219
-
220
- ### 3. Test User Behavior
221
- ```typescript
222
- // ✅ Good - Test what users see/do
223
- expect(screen.getByText('Welcome, John')).toBeInTheDocument();
224
- await user.click(screen.getByRole('button'));
225
-
226
- // ❌ Bad - Test implementation details
227
- expect(component.state.isVisible).toBe(true);
228
- expect(component.props.onClick).toHaveBeenCalled();
229
- ```
230
-
231
- ### 4. Mock External Dependencies
232
- ```typescript
233
- // Mock API calls
234
- vi.mock('../api/users', () => ({
235
- fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'John' })
236
- }));
237
-
238
- // Mock modules
239
- vi.mock('react-router-dom', () => ({
240
- useNavigate: () => vi.fn()
241
- }));
242
- ```
243
-
244
- ### 5. Clean Up After Tests
245
- ```typescript
246
- afterEach(() => {
247
- cleanup();
248
- vi.clearAllMocks();
249
- });
250
- ```
251
-
252
- ## 🔄 Common Patterns
253
-
254
- ### 1. Component Testing Pattern
255
- ```typescript
256
- describe('Button Component', () => {
257
- describe('Rendering', () => {
258
- it('renders with text', () => {
259
- render(<Button>Click me</Button>);
260
- expect(screen.getByRole('button')).toBeInTheDocument();
261
- });
262
- });
263
-
264
- describe('Event Handling', () => {
265
- it('handles click events', async () => {
266
- const handleClick = vi.fn();
267
- const user = userEvent.setup();
268
-
269
- render(<Button onClick={handleClick}>Click me</Button>);
270
- await user.click(screen.getByRole('button'));
271
-
272
- expect(handleClick).toHaveBeenCalledTimes(1);
273
- });
274
- });
275
- });
276
- ```
277
-
278
- ### 2. Hook Testing Pattern
279
- ```typescript
280
- describe('useCounter Hook', () => {
281
- it('initializes with default value', () => {
282
- const { result } = renderHook(() => useCounter());
283
- expect(result.current.count).toBe(0);
284
- });
285
-
286
- it('increments count', () => {
287
- const { result } = renderHook(() => useCounter(0));
288
-
289
- act(() => {
290
- result.current.increment();
291
- });
292
-
293
- expect(result.current.count).toBe(1);
294
- });
295
- });
296
- ```
297
-
298
- ### 3. Integration Testing Pattern
299
- ```typescript
300
- describe('User Profile Integration', () => {
301
- it('loads and displays user data', async () => {
302
- render(<UserProfile userId="1" />);
303
-
304
- await waitFor(() => {
305
- expect(screen.getByText('John Doe')).toBeInTheDocument();
306
- });
307
- });
308
- });
309
- ```
310
-
311
- ## ❌ Anti-Patterns
312
-
313
- ### 1. Testing Implementation Details
314
- ```typescript
315
- // ❌ Bad
316
- expect(component.state.isVisible).toBe(true);
317
- expect(component.props.onClick).toHaveBeenCalled();
318
-
319
- // ✅ Good
320
- expect(screen.getByText('Visible content')).toBeInTheDocument();
321
- await user.click(screen.getByRole('button'));
322
- ```
323
-
324
- ### 2. Over-Mocking
325
- ```typescript
326
- // ❌ Bad - Mocking everything
327
- vi.mock('../utils/formatDate');
328
- vi.mock('../hooks/useAuth');
329
- vi.mock('../components/Button');
330
-
331
- // ✅ Good - Mock only what's necessary
332
- vi.mock('../api/users');
333
- ```
334
-
335
- ### 3. Testing Multiple Things in One Test
336
- ```typescript
337
- // ❌ Bad
338
- it('renders button and handles click and shows loading', () => {
339
- // Too many assertions
340
- });
341
-
342
- // ✅ Good
343
- it('renders button', () => {});
344
- it('handles click', () => {});
345
- it('shows loading state', () => {});
346
- ```
347
-
348
- ### 4. Brittle Selectors
349
- ```typescript
350
- // ❌ Bad - Fragile selectors
351
- screen.getByClassName('btn-primary');
352
- screen.getByTestId('submit-button');
353
-
354
- // ✅ Good - Semantic selectors
355
- screen.getByRole('button', { name: 'Submit' });
356
- screen.getByLabelText('Email address');
357
- ```
358
-
359
- ## 📊 Code Coverage
360
-
361
- ### Coverage Targets
362
- - **Statements**: 80%
363
- - **Branches**: 80%
364
- - **Functions**: 80%
365
- - **Lines**: 80%
366
-
367
- ### What to Test
368
- - ✅ Happy paths
369
- - ✅ Error conditions
370
- - ✅ Edge cases
371
- - ✅ User interactions
372
- - ✅ State changes
373
-
374
- ### What NOT to Test
375
- - ❌ Third-party library code
376
- - ❌ Generated code
377
- - ❌ Configuration files
378
- - ❌ Type definitions
379
-
380
- ## 🧩 Test Expansion Workflow
381
-
382
- When expanding test coverage:
383
-
384
- 1. **Start with the coverage report** (`npm test -- --coverage`)
385
- 2. Identify high-priority or under-tested modules
386
- 3. Choose the appropriate test type (unit, component, integration)
387
- 4. Use our standard structure and semantic queries
388
- 5. Assert observable behaviour (not internal state or styles)
389
- 6. Use test tags (`[unit]`, `[integration]`, etc.) for discoverability
390
-
391
- ## ✅ Pre-Merge Test Checklist
392
-
393
- - [ ] Are all tests passing (no `it.only` or `test.skip`)?
394
- - [ ] Are coverage thresholds still met?
395
- - [ ] Are tests colocated with the source?
396
- - [ ] Are semantic queries used (no test IDs)?
397
- - [ ] Is global state cleaned up properly?
398
- - [ ] Are new utilities or hooks covered with tests?
399
-
400
- ## 🚀 Running Tests
401
-
402
- ### Commands
403
- ```bash
404
- # Run all tests
405
- npm test
406
-
407
- # Run tests in watch mode
408
- npm test -- --watch
409
-
410
- # Run tests with coverage
411
- npm test -- --coverage
412
-
413
- # Run specific test file
414
- npm test -- Button.test.tsx
415
-
416
- # Run tests matching pattern
417
- npm test -- --grep "Button"
418
- ```
419
-
420
- ### Debugging Tests
421
- ```typescript
422
- // Debug output
423
- screen.debug();
424
-
425
- // Debug specific element
426
- screen.debug(screen.getByRole('button'));
427
-
428
- // Log all queries
429
- screen.logTestingPlaygroundURL();
430
- ```
431
-
432
- ## 📚 Resources
433
-
434
- - [React Testing Library Docs](https://testing-library.com/docs/react-testing-library/intro/)
435
- - [Vitest Docs](https://vitest.dev/)
436
- - [Testing Best Practices](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)
437
- - [Jest DOM Matchers](https://github.com/testing-library/jest-dom)
438
- ---
439
-
440
- ## 🔖 Test Type Tags
441
-
442
- Use explicit tags to clarify the type of test being written:
443
-
444
- - `[unit]` – for isolated logic (e.g. pure functions, hooks)
445
- - `[component]` – for rendering UI components with interactions
446
- - `[integration]` – for multi-module tests or user journeys
447
-
448
- **Example:**
449
-
450
- ```ts
451
- describe('[unit] useDebounce', () => {
452
- it('should delay the update', () => { ... })
453
- })
454
- ```
455
-
456
- ---
457
-
458
- ## 🚫 Skipped Tests Policy
459
-
460
- - All `it.skip` or `test.skip` must include a comment explaining why, and reference a JIRA or GitHub issue.
461
- - Example:
462
-
463
- ```ts
464
- it.skip('fails intermittently in CI – see GH#456', () => { ... })
465
- ```
466
-
467
- ---
468
-
469
- ## 🗂 File Placement
470
-
471
- - ✅ Unit/component tests should be colocated with their source file.
472
- - ✅ Integration tests go in `src/__tests__/integration/`.
473
- - ✅ Test utilities, fixtures, and mock setup belong in `src/__tests__/helpers/` or `src/__tests__/fixtures/`.
474
-
475
- ---
476
-
477
- ## 🧑‍🎨 Style-Resilient Test Practices
478
-
479
- Tests should **never assert class names, layout structure, or DOM nesting** unless explicitly testing responsive/UI logic.
480
- Instead, assert **visible output and interactive behaviour** that users experience (e.g. text content, button states, aria roles, form values).
481
- This ensures tests remain stable through valid styling and layout changes.
482
-
483
- ---
484
-
485
- ## 🚀 Performance Best Practices
486
-
487
- - Avoid global mocks unless necessary.
488
- - Prefer `beforeEach` over `beforeAll` to keep tests isolated.
489
- - Minimise deep setup logic—use lightweight, focused factories.
490
- - Use `vi.useFakeTimers()` for time-sensitive logic.
491
- - Monitor heap usage with `--logHeapUsage` flag for memory-intensive tests.
492
- - Reduce parallelization if encountering memory issues.
493
-
494
- ---
495
-
496
- ## 🧹 Memory Leak Prevention
497
-
498
- ### Common Causes of Memory Leaks
499
- 1. **Unmounted timers** - setInterval/setTimeout not cleared
500
- 2. **Event listeners** - Not removed in cleanup
501
- 3. **Subscriptions** - Not unsubscribed
502
- 4. **DOM references** - Lingering references to removed elements
503
- 5. **Global state** - Not reset between tests
504
- 6. **Mocks** - Not cleared after tests
505
-
506
- ### Prevention Checklist
507
- ```typescript
508
- describe('Component with Resources', () => {
509
- beforeEach(() => {
510
- // Setup fake timers if using setTimeout/setInterval
511
- vi.useFakeTimers();
512
- });
513
-
514
- afterEach(() => {
515
- // Clear all timers
516
- vi.clearAllTimers();
517
- vi.useRealTimers();
518
-
519
- // React Testing Library cleanup (unmount components)
520
- cleanup();
521
-
522
- // Clear all mocks
523
- vi.clearAllMocks();
524
-
525
- // Clear storage
526
- localStorage.clear();
527
- sessionStorage.clear();
528
- });
529
-
530
- it('handles async operations', async () => {
531
- // test implementation
532
- });
533
- });
534
- ```
535
-
536
- ### Detecting Memory Leaks
537
- ```bash
538
- # Run tests with heap logging
539
- npm test -- --logHeapUsage
540
-
541
- # Look for tests showing increasing memory usage
542
- # Baseline: 44-50 MB
543
- # Warning: 50-55 MB
544
- # Critical: >55 MB
545
- ```
546
-
547
- ---
548
-
549
- ## ⏱️ Async Testing Best Practices
550
-
551
- ### Use findBy Over getBy + waitFor
552
- ```typescript
553
- // ❌ Bad - Inefficient
554
- await waitFor(() => {
555
- expect(screen.getByRole('button')).toBeInTheDocument();
556
- });
557
-
558
- // ✅ Good - Built-in waiting
559
- const button = await screen.findByRole('button');
560
- expect(button).toBeInTheDocument();
561
- ```
562
-
563
- ### Set Appropriate Timeouts
564
- ```typescript
565
- // For fast synchronous operations - don't use waitFor
566
- expect(result.current.value).toBe('test');
567
-
568
- // For quick async operations
569
- await waitFor(() => {
570
- expect(result.current.isLoading).toBe(false);
571
- }, { timeout: 100, interval: 10 });
572
-
573
- // For slow operations only
574
- await waitFor(() => {
575
- expect(result.current.data).toBeDefined();
576
- }, { timeout: 5000 });
577
- ```
578
-
579
- ### waitFor Usage Guidelines
580
- - **DON'T** use waitFor for synchronous operations
581
- - **DO** use findBy queries when waiting for elements
582
- - **DO** set explicit timeouts based on expected operation speed
583
- - **DO** add interval to reduce polling frequency for slow operations
584
- - **DON'T** nest waitFor calls (causes exponential delays)
585
-
586
- ### Common waitFor Mistakes
587
- ```typescript
588
- // ❌ Bad - Waiting for sync operation
589
- const { result } = renderHook(() => useCounter(0));
590
- await waitFor(() => {
591
- expect(result.current.count).toBe(0); // This is synchronous!
592
- });
593
-
594
- // ✅ Good - Direct assertion
595
- const { result } = renderHook(() => useCounter(0));
596
- expect(result.current.count).toBe(0);
597
-
598
- // ❌ Bad - Generic timeout without condition
599
- await waitFor(() => {}, { timeout: 1000 }); // Just waiting
600
-
601
- // ✅ Good - Wait for specific condition
602
- await waitFor(() => {
603
- expect(result.current.isLoading).toBe(false);
604
- });
605
- ```
606
-
607
- ---
608
-
609
- ## 🔧 Mocking Complex APIs
610
-
611
- ### Mocking Functions with Multiple Call Signatures
612
- When testing code that calls a mocked function multiple times with different parameters:
613
-
614
- ```typescript
615
- // ❌ Bad - mockResolvedValue returns same data for all calls
616
- mockSupabase.rpc.mockResolvedValue({ data: [...], error: null });
617
-
618
- // ✅ Good - mockImplementation handles different calls
619
- mockSupabase.rpc.mockImplementation((functionName: string, params) => {
620
- if (functionName === 'get_user') {
621
- return Promise.resolve({ data: { id: '1' }, error: null });
622
- }
623
- if (functionName === 'get_permissions') {
624
- return Promise.resolve({ data: [...], error: null });
625
- }
626
- return Promise.resolve({ data: null, error: null });
627
- });
628
- ```
629
-
630
- ### Example: useRBAC Hook Mocking
631
- ```typescript
632
- // useRBAC makes TWO RPC calls - must mock both
633
- const setupRBACMock = (permissions: any[] = []) => {
634
- mockSupabase.rpc.mockImplementation((functionName, params) => {
635
- if (functionName === 'util_app_resolve') {
636
- return Promise.resolve({
637
- data: [{ app_id: 'test-app-id', has_access: true }],
638
- error: null
639
- });
640
- }
641
- if (functionName === 'rbac_permissions_get') {
642
- return Promise.resolve({ data: permissions, error: null });
643
- }
644
- return Promise.resolve({ data: null, error: null });
645
- });
646
- };
647
-
648
- // Usage in tests
649
- it('loads user permissions', async () => {
650
- setupRBACMock([
651
- { permission_type: 'all_permissions', role_name: 'super_admin' }
652
- ]);
653
-
654
- const { result } = renderHook(() => useRBAC());
655
- await waitFor(() => {
656
- expect(result.current.globalRole).toBe('super_admin');
657
- });
658
- });
659
- ```
660
-
661
- ---
662
-
663
- ## 🔐 RBAC Testing Patterns
664
-
665
- When testing RBAC components, providers, and hooks, follow these patterns to ensure comprehensive coverage of security-critical functionality.
666
-
667
- ### Testing RBAC Providers
668
-
669
- #### Organisation Context Integration
670
- ```typescript
671
- describe('RBACProvider', () => {
672
- it('requires organisation context when set', () => {
673
- const { result } = renderHook(() => useRBAC());
674
-
675
- expect(() => result.current.requireOrganisationContext()).toThrow();
676
- });
677
-
678
- it('returns organisation ID when context is available', () => {
679
- renderWithProviders(<App />, { organisationId: 'org-123' });
680
- const { result } = renderHook(() => useRBAC());
681
-
682
- expect(result.current.requireOrganisationContext()).toBe('org-123');
683
- });
684
- });
685
- ```
686
-
687
- #### Permission Refresh Logic
688
- ```typescript
689
- describe('RBACProvider', () => {
690
- it('refreshes permissions when event changes', async () => {
691
- const mockPermissions = [
692
- { permission_type: 'event_app_access', role_name: 'planner' }
693
- ];
694
-
695
- setupRBACMock(mockPermissions);
696
-
697
- const { result } = renderHook(() => useRBAC());
698
-
699
- // Initial permissions
700
- expect(result.current.isLoading).toBe(true);
701
-
702
- await waitFor(() => {
703
- expect(result.current.isLoading).toBe(false);
704
- });
705
-
706
- // Change event
707
- act(() => {
708
- result.current.setSelectedEventId('new-event-id');
709
- });
710
-
711
- await waitFor(() => {
712
- expect(result.current.rbacLoading).toBe(false);
713
- });
714
- });
715
- });
716
- ```
717
-
718
- ### Testing Permission Hooks
719
-
720
- #### Cache vs No-Cache Behavior
721
- ```typescript
722
- describe('usePermissions Hook', () => {
723
- it('uses cached results when useCache is true', async () => {
724
- const setupPermissionsMock = () => {
725
- mockSupabase.rpc.mockImplementation((functionName) => {
726
- if (functionName === 'rbac_permissions_get') {
727
- return Promise.resolve({
728
- data: [{ permission_type: 'read:users', role_name: 'viewer' }],
729
- error: null
730
- });
731
- }
732
- return Promise.resolve({ data: null, error: null });
733
- });
734
- };
735
-
736
- setupPermissionsMock();
737
-
738
- const { result, rerender } = renderHook(
739
- ({ cache }) => usePermissions('user-123', scope, { useCache: cache }),
740
- { initialProps: { cache: true } }
741
- );
742
-
743
- await waitFor(() => {
744
- expect(result.current.permissions).toBeDefined();
745
- });
746
-
747
- // Second call should use cache (no new API call expected)
748
- rerender({ cache: true });
749
-
750
- // Verify API was only called once (would need mock tracking)
751
- });
752
- });
753
- ```
754
-
755
- #### Multi-Permission Validation
756
- ```typescript
757
- describe('useHasAnyPermission Hook', () => {
758
- it('returns true if user has any of the required permissions', async () => {
759
- const mockPermissions = [
760
- { permission_type: 'read:users', role_name: 'viewer' }
761
- ];
762
-
763
- setupRBACMock(mockPermissions);
764
-
765
- const { result } = renderHook(() =>
766
- useHasAnyPermission('user-123', scope, ['read:users', 'write:users'])
767
- );
768
-
769
- await waitFor(() => {
770
- expect(result.current.hasAny).toBe(true);
771
- });
772
- });
773
-
774
- it('returns false if user has none of the required permissions', async () => {
775
- const mockPermissions = [];
776
-
777
- setupRBACMock(mockPermissions);
778
-
779
- const { result } = renderHook(() =>
780
- useHasAnyPermission('user-123', scope, ['read:users', 'write:users'])
781
- );
782
-
783
- await waitFor(() => {
784
- expect(result.current.hasAny).toBe(false);
785
- });
786
- });
787
- });
788
- ```
789
-
790
- ### Testing Error Recovery
791
-
792
- ```typescript
793
- describe('RBACProvider', () => {
794
- it('handles permission fetch errors gracefully', async () => {
795
- const mockError = new Error('Permission fetch failed');
796
-
797
- mockSupabase.rpc.mockImplementation(() => {
798
- return Promise.resolve({ data: null, error: mockError });
799
- });
800
-
801
- const { result } = renderHook(() => useRBAC());
802
-
803
- await waitFor(() => {
804
- expect(result.current.rbacError).toBeDefined();
805
- expect(result.current.rbacError?.message).toContain('Permission fetch failed');
806
- });
807
-
808
- // Verify permissions are cleared on error
809
- expect(result.current.permissions).toEqual({});
810
- });
811
-
812
- it('allows retry after error', async () => {
813
- // Setup initial error
814
- mockSupabase.rpc.mockRejectedValueOnce(new Error('Network error'));
815
-
816
- const { result } = renderHook(() => useRBAC());
817
-
818
- await waitFor(() => {
819
- expect(result.current.rbacError).toBeDefined();
820
- });
821
-
822
- // Setup successful response for retry
823
- mockSupabase.rpc.mockResolvedValueOnce({
824
- data: [{ permission_type: 'read:users' }],
825
- error: null
826
- });
827
-
828
- await act(async () => {
829
- await result.current.refreshPermissions();
830
- });
831
-
832
- expect(result.current.rbacError).toBeNull();
833
- expect(result.current.permissions).toBeDefined();
834
- });
835
- });
836
- ```
837
-
838
- ### Mocking RBAC RPC Functions
839
-
840
- When testing RBAC components, ensure all RPC calls are properly mocked:
841
-
842
- ```typescript
843
- const setupRBACMock = (permissions: any[] = [], orgContext = true) => {
844
- mockSupabase.rpc.mockImplementation((functionName, params) => {
845
- if (functionName === 'util_app_resolve') {
846
- return Promise.resolve({
847
- data: [{ app_id: 'test-app-id', has_access: true }],
848
- error: null
849
- });
850
- }
851
-
852
- if (functionName === 'rbac_permissions_get') {
853
- // Always include organisation context in RBAC calls
854
- expect(params).toHaveProperty('p_organisation_id');
855
- if (!orgContext) {
856
- return Promise.resolve({ data: [], error: null });
857
- }
858
-
859
- return Promise.resolve({ data: permissions, error: null });
860
- }
861
-
862
- return Promise.resolve({ data: null, error: null });
863
- });
864
- };
865
- ```
866
-
867
- ### Testing Complete RBAC Workflows
868
-
869
- ```typescript
870
- describe('Complete RBAC Workflow', () => {
871
- it('handles full user journey: login → select org → select event → check permissions', async () => {
872
- // 1. User logs in
873
- const { result: authResult } = renderHook(() => useAuth());
874
-
875
- await act(async () => {
876
- await authResult.current.signIn({
877
- email: 'user@example.com',
878
- password: 'password'
879
- });
880
- });
881
-
882
- // 2. User selects organisation
883
- const { result: orgResult } = renderHook(() => useOrganisationContext());
884
-
885
- await act(async () => {
886
- await orgResult.current.setSelectedOrganisationId('org-123');
887
- });
888
-
889
- // 3. User selects event
890
- const { result: rbacResult } = renderHook(() => useRBAC());
891
-
892
- await act(async () => {
893
- rbacResult.current.setSelectedEventId('event-456');
894
- });
895
-
896
- // 4. Verify permissions are loaded
897
- await waitFor(() => {
898
- expect(rbacResult.current.rbacLoading).toBe(false);
899
- });
900
-
901
- expect(rbacResult.current.hasPermission('read:users')).toBe(true);
902
- });
903
- });
904
- ```
905
-
906
- ---
907
-
908
- ## 🔌 Service Testing Patterns
909
-
910
- When testing services (Auth, Event, Organisation, RBAC, Security), ensure comprehensive coverage of API interactions, error handling, and data transformations.
911
-
912
- ### Testing Service Methods
913
-
914
- #### Mocking Supabase Client
915
- ```typescript
916
- describe('AuthService', () => {
917
- let mockSupabase: ReturnType<typeof createMockSupabaseClient>;
918
- let authService: AuthService;
919
-
920
- beforeEach(() => {
921
- mockSupabase = createMockSupabaseClient();
922
- authService = new AuthService(mockSupabase);
923
- });
924
-
925
- it('signs in user successfully', async () => {
926
- const mockUser = { id: '123', email: 'user@example.com' };
927
-
928
- mockSupabase.auth.signInWithPassword.mockResolvedValue({
929
- data: { user: mockUser, session: {} },
930
- error: null
931
- });
932
-
933
- const result = await authService.signIn({
934
- email: 'user@example.com',
935
- password: 'password'
936
- });
937
-
938
- expect(result).toBeDefined();
939
- expect(result.user).toEqual(mockUser);
940
- expect(mockSupabase.auth.signInWithPassword).toHaveBeenCalledWith({
941
- email: 'user@example.com',
942
- password: 'password'
943
- });
944
- });
945
- });
946
- ```
947
-
948
- ### Testing Error Scenarios
949
-
950
- ```typescript
951
- describe('EventService', () => {
952
- it('handles fetch errors gracefully', async () => {
953
- const mockError = new Error('Network error');
954
-
955
- mockSupabase.from().select().mockRejectedValue(mockError);
956
-
957
- await expect(eventService.fetchEvent('event-id')).rejects.toThrow();
958
- });
959
-
960
- it('returns null for non-existent events', async () => {
961
- mockSupabase.from().select().mockResolvedValue({
962
- data: null,
963
- error: null
964
- });
965
-
966
- const result = await eventService.fetchEvent('non-existent-id');
967
-
968
- expect(result).toBeNull();
969
- });
970
- });
971
- ```
972
-
973
- ### Testing Data Transformations
974
-
975
- ```typescript
976
- describe('OrganisationService', () => {
977
- it('transforms raw database data to entity format', async () => {
978
- const rawData = {
979
- id: 'org-123',
980
- name: 'Test Organisation',
981
- created_at: '2024-01-01T00:00:00Z'
982
- };
983
-
984
- mockSupabase.from().select().mockResolvedValue({
985
- data: [rawData],
986
- error: null
987
- });
988
-
989
- const result = await organisationService.fetchOrganisation('org-123');
990
-
991
- expect(result).toMatchObject({
992
- id: 'org-123',
993
- name: 'Test Organisation',
994
- createdAt: expect.any(Date)
995
- });
996
- });
997
- });
998
- ```
999
-
1000
- ### Testing Subscription Handling
1001
-
1002
- ```typescript
1003
- describe('EventService with Realtime', () => {
1004
- it('subscribes to event updates', () => {
1005
- const callback = vi.fn();
1006
-
1007
- eventService.subscribeToEvent('event-id', callback);
1008
-
1009
- expect(mockSupabase.channel).toHaveBeenCalled();
1010
-
1011
- const channelMock = mockSupabase.channel.mock.results[0].value;
1012
- expect(channelMock.on).toHaveBeenCalledWith(
1013
- 'postgres_changes',
1014
- expect.objectContaining({
1015
- event: 'UPDATE',
1016
- schema: 'public',
1017
- table: 'event'
1018
- }),
1019
- callback
1020
- );
1021
- });
1022
-
1023
- it('unsubscribes on cleanup', () => {
1024
- const callback = vi.fn();
1025
- const unsubscribe = eventService.subscribeToEvent('event-id', callback);
1026
-
1027
- const channelMock = mockSupabase.channel.mock.results[0].value;
1028
-
1029
- unsubscribe();
1030
-
1031
- expect(channelMock.unsubscribe).toHaveBeenCalled();
1032
- });
1033
- });
1034
- ```
1035
-
1036
- ### Testing Service Error Recovery
1037
-
1038
- ```typescript
1039
- describe('RBACService', () => {
1040
- it('implements retry logic for transient errors', async () => {
1041
- let callCount = 0;
1042
-
1043
- mockSupabase.rpc.mockImplementation(() => {
1044
- callCount++;
1045
- if (callCount === 1) {
1046
- return Promise.resolve({ data: null, error: { code: 'PGRST116' } });
1047
- }
1048
- return Promise.resolve({ data: [{ permission_type: 'read' }], error: null });
1049
- });
1050
-
1051
- const result = await rbacService.getPermissions('user-id');
1052
-
1053
- expect(result).toBeDefined();
1054
- expect(callCount).toBe(2);
1055
- });
1056
- });
1057
- ```
1058
-
1059
- ### Verifying API Call Parameters
1060
-
1061
- ```typescript
1062
- describe('AuthService', () => {
1063
- it('includes all required parameters in API calls', async () => {
1064
- await authService.updateUser({ name: 'John Doe' });
1065
-
1066
- expect(mockSupabase.auth.updateUser).toHaveBeenCalledWith({
1067
- data: { name: 'John Doe' }
1068
- });
1069
- });
1070
-
1071
- it('handles organisation context in RBAC calls', async () => {
1072
- await rbacService.getPermissions('user-id', 'org-123', 'event-456');
1073
-
1074
- expect(mockSupabase.rpc).toHaveBeenCalledWith(
1075
- 'rbac_permissions_get',
1076
- expect.objectContaining({
1077
- p_user_id: 'user-id',
1078
- p_organisation_id: 'org-123',
1079
- p_event_id: 'event-456'
1080
- })
1081
- );
1082
- });
1083
- });
1084
- ```
1085
-
1086
- ---
1087
-
1088
- ## 🎨 jsdom Configuration
1089
-
1090
- ### Enable Visual Testing Features
1091
- To prevent `getComputedStyle` and other visual API errors:
1092
-
1093
- ```typescript
1094
- // vitest.config.ts
1095
- export default defineConfig({
1096
- test: {
1097
- environment: 'jsdom',
1098
- environmentOptions: {
1099
- jsdom: {
1100
- pretendToBeVisual: true, // Enables getComputedStyle
1101
- resources: 'usable', // Enables resource loading
1102
- }
1103
- }
1104
- }
1105
- });
1106
- ```
1107
-
1108
- ### Benefits
1109
- - ✅ Automatic getComputedStyle support
1110
- - ✅ Proper CSS property access
1111
- - ✅ Window.matchMedia support
1112
- - ✅ Element.getBoundingClientRect support
1113
- - ✅ No manual mocking needed
1114
-
1115
- ---
1116
-
1117
- ## 🚫 Common Test Anti-Patterns
1118
-
1119
- ### 1. Testing with Wrong Mock Approach
1120
- ```typescript
1121
- // ❌ Bad - Single mockResolvedValue for multiple calls
1122
- mockApi.fetch.mockResolvedValue(data);
1123
- // Problem: All calls get same data, even if they should differ
1124
-
1125
- // ✅ Good - mockImplementation for different calls
1126
- mockApi.fetch.mockImplementation((endpoint) => {
1127
- if (endpoint === '/users') return Promise.resolve(users);
1128
- if (endpoint === '/posts') return Promise.resolve(posts);
1129
- return Promise.resolve(null);
1130
- });
1131
- ```
1132
-
1133
- ### 2. Not Accounting for Multiple API Calls
1134
- ```typescript
1135
- // ❌ Bad - Assumes hook makes one call
1136
- mockApi.getData.mockResolvedValue(testData);
1137
-
1138
- // ✅ Good - Account for all calls hook makes
1139
- mockApi.getData.mockImplementation((type) => {
1140
- if (type === 'config') return Promise.resolve(config);
1141
- if (type === 'data') return Promise.resolve(testData);
1142
- return Promise.resolve(null);
1143
- });
1144
- ```
1145
-
1146
- ### 3. Insufficient Async Handling
1147
- ```typescript
1148
- // ❌ Bad - Not waiting for async updates
1149
- const { result } = renderHook(() => useAsync());
1150
- expect(result.current.data).toBeDefined(); // May still be loading!
1151
-
1152
- // ✅ Good - Wait for async completion
1153
- const { result } = renderHook(() => useAsync());
1154
- await waitFor(() => {
1155
- expect(result.current.isLoading).toBe(false);
1156
- });
1157
- expect(result.current.data).toBeDefined();
1158
- ```
1159
-
1160
- ---
1161
-
1162
- ## 🎯 Coverage Expectations
1163
-
1164
- Minimum thresholds per category:
1165
-
1166
- - Core utils/hooks: **≥ 95%**
1167
- - UI components: **≥ 90%**
1168
- - Feature modules: **≥ 85%**
1169
- - Overall project: **≥ 82%** (targeting continuous improvement)
1170
-
1171
- ## 📊 Coverage Thresholds
1172
-
1173
- | Type | Target | Block CI | Rationale |
1174
- |------|--------|----------|-----------|
1175
- | **Utils/Hooks** | 95% | Yes | Core business logic, high usage |
1176
- | **UI Components** | 90% | Yes | User-facing, must be reliable |
1177
- | **Services** | 85% | Yes | API interactions, critical paths |
1178
- | **Validation** | 95% | Yes | Security and data integrity |
1179
- | **Overall** | 82% | Yes | Maintains quality baseline |
1180
-
1181
- ### Enforcing Coverage Thresholds
1182
-
1183
- ```typescript
1184
- // vitest.config.ts
1185
- coverage: {
1186
- thresholds: {
1187
- lines: 82,
1188
- branches: 80,
1189
- functions: 80,
1190
- statements: 82,
1191
-
1192
- // Per-file thresholds
1193
- 'src/hooks/**': {
1194
- lines: 95,
1195
- branches: 95,
1196
- functions: 95
1197
- },
1198
- 'src/components/**': {
1199
- lines: 90,
1200
- branches: 90,
1201
- functions: 90
1202
- },
1203
- 'src/services/**': {
1204
- lines: 85,
1205
- branches: 85,
1206
- functions: 85
1207
- }
1208
- }
1209
- }
1210
- ```
1211
-
1212
- ### Coverage Goals by Module
1213
-
1214
- **High Priority (Security & Business Logic)**:
1215
- - RBAC Providers: **≥ 90%** (critical for security)
1216
- - Permission Hooks: **≥ 90%** (used throughout app)
1217
- - Authentication Services: **≥ 90%** (security-critical)
1218
- - Validation Functions: **≥ 95%** (data integrity)
1219
-
1220
- **Medium Priority (Core Features)**:
1221
- - UI Components: **≥ 90%** (user experience)
1222
- - Service Layer: **≥ 85%** (API interactions)
1223
- - Providers: **≥ 85%** (state management)
1224
-
1225
- **Acceptable Gaps**:
1226
- - Type definitions (interfaces, types only): **0%** (acceptable)
1227
- - Auto-generated code: **0%** (acceptable)
1228
- - Example components: **0%** (not in production)
1229
- - DataTable subcomponents with **intentional low coverage** (tested via integration tests)
1230
-
1231
- ---
1232
-
1233
- ## 🧩 DataTable Testing Patterns
1234
-
1235
- The DataTable component and its subcomponents follow a specific testing strategy that prioritizes integration tests over isolated unit tests for tightly-coupled components.
1236
-
1237
- ### Intentional Low Coverage on Subcomponents
1238
-
1239
- Several DataTable subcomponents show low coverage percentages (0-15%):
1240
- - `ColumnFilter.tsx` - 5.81%
1241
- - `FilterRow.tsx` - 2.77%
1242
- - `EditableRow.tsx` - 5.14%
1243
- - `DraggableColumnHeader.tsx` - 0%
1244
- - `ViewRowModal.tsx` - 0%
1245
- - `ExpandButton.tsx` - 0%
1246
- - `GroupHeader.tsx` - 11.53%
1247
- - `DataTableBody.tsx` - 0%
1248
-
1249
- **This is intentional and correct** - these components are comprehensively tested through DataTable integration tests.
1250
-
1251
- ### Why Integration Tests Over Unit Tests?
1252
-
1253
- 1. **Tight Coupling with TanStack React Table**: These subcomponents depend heavily on table internals that would require extensive mocking
1254
- 2. **Behavior Over Implementation**: Integration tests verify actual user interactions rather than internal implementation details
1255
- 3. **Prevent Brittle Tests**: Isolated unit tests would be fragile and break on any internal refactoring
1256
- 4. **User Value Focus**: Users interact with DataTable as a whole, not individual subcomponents
1257
-
1258
- ### DataTable Testing Strategy
1259
-
1260
- ```typescript
1261
- // ✅ Good - Integration test for DataTable workflows
1262
- describe('DataTable - Editing Workflow', () => {
1263
- it('allows editing rows with save/cancel actions', async () => {
1264
- render(<DataTable data={mockData} onSave={mockSave} />);
1265
- await user.click(screen.getByRole('button', { name: 'Edit' }));
1266
- await user.type(screen.getByRole('textbox'), 'Updated Value');
1267
- await user.click(screen.getByRole('button', { name: 'Save' }));
1268
- expect(mockSave).toHaveBeenCalledWith(expectedData);
1269
- });
1270
- });
1271
-
1272
- // ❌ Bad - Unit test for isolated subcomponent
1273
- // Would require mocking TanStack table internals
1274
- describe('EditableRow Component', () => {
1275
- it('handles edit state', () => {
1276
- // Tests implementation, not behavior
1277
- });
1278
- });
1279
- ```
1280
-
1281
- ### Where to Add Tests
1282
-
1283
- **✅ Test These:**
1284
- - Core DataTable component logic (`DataTable.tsx`)
1285
- - DataTable context and state management
1286
- - Feature configuration logic
1287
- - Data transformation utilities
1288
-
1289
- **❌ Don't Test These (Acceptable Low Coverage):**
1290
- - TanStack table subcomponent wrappers
1291
- - Presentation-only rendering components
1292
- - Components tested via integration tests
1293
-
1294
- ### Coverage Exemption
1295
-
1296
- Low coverage on DataTable subcomponents is **explicitly exempt** from coverage thresholds. The integration test suite provides comprehensive coverage:
1297
-
1298
- - **Workflow Validation**: 23 tests covering all user interactions
1299
- - **Regression Fixes**: 13 tests preventing known issues
1300
- - **Sorting**: Comprehensive sorting functionality tests
1301
- - **Data Integrity**: Validation of data consistency
1302
-
1303
- **Reference**: See `packages/core/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md` for complete justification.
1304
-
1305
- ---
1306
-
1307
- ## 📚 Additional Resources
1308
-
1309
- - [Vitest Mocking Guide](https://vitest.dev/guide/mocking.html)
1310
- - [Testing Library Best Practices](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)
1311
- - [React Hooks Testing](https://react-hooks-testing-library.com/)
1312
- - [jsdom Documentation](https://github.com/jsdom/jsdom)
1313
-
1314
- ### Internal Documentation
1315
- - [TEST_FAILURE_ANALYSIS.md](../../../TEST_FAILURE_ANALYSIS.md) - Common test failures and fixes
1316
- - [MEMORY_PERFORMANCE_ANALYSIS.md](../../../MEMORY_PERFORMANCE_ANALYSIS.md) - Performance optimization guide
1317
- - [TEST_SUITE_IMPLEMENTATION_GUIDE.md](../../../TEST_SUITE_IMPLEMENTATION_GUIDE.md) - Step-by-step implementation guide
1318
-
1319
- ---
1320
-
1321
- ## 🌐 PublicLayout Testing Patterns
1322
-
1323
- PublicLayout components are designed to be completely isolated from authentication and organisation context for public-facing pages.
1324
-
1325
- ### Testing PublicPageProvider
1326
-
1327
- **Key Principles**:
1328
- - Test environment variable handling (Vite & Node.js environments)
1329
- - Test Supabase client creation from environment variables
1330
- - Test error boundary integration
1331
- - Verify no authentication context triggers
1332
-
1333
- ```typescript
1334
- describe('PublicPageProvider', () => {
1335
- it('provides Supabase client when environment variables are present', () => {
1336
- render(
1337
- <PublicPageProvider>
1338
- <TestComponent />
1339
- </PublicPageProvider>
1340
- );
1341
-
1342
- expect(screen.getByTestId('has-client')).toBeInTheDocument();
1343
- });
1344
-
1345
- it('provides isPublicPage flag in context', () => {
1346
- const { result } = renderHook(() => usePublicPageContext(), {
1347
- wrapper: ({ children }) => (
1348
- <PublicPageProvider>{children}</PublicPageProvider>
1349
- )
1350
- });
1351
-
1352
- expect(result.current.isPublicPage).toBe(true);
1353
- });
1354
-
1355
- it('wraps children with PublicErrorBoundary', () => {
1356
- const { container } = render(
1357
- <PublicPageProvider>
1358
- <div>Test Content</div>
1359
- </PublicPageProvider>
1360
- );
1361
-
1362
- // Should render children through error boundary
1363
- expect(screen.getByText('Test Content')).toBeInTheDocument();
1364
- });
1365
- });
1366
- ```
1367
-
1368
- ### Testing PublicPageDebugger
1369
-
1370
- **Key Principles**:
1371
- - Test console logging when enabled
1372
- - Test context detection logic
1373
- - Test enable/disable state changes
1374
-
1375
- ```typescript
1376
- describe('PublicPageDebugger', () => {
1377
- it('logs context information when enabled', async () => {
1378
- const consoleSpy = vi.spyOn(console, 'log');
1379
-
1380
- render(
1381
- <PublicPageProvider>
1382
- <PublicPageDebugger enabled={true} label="TestDebugger" />
1383
- </PublicPageProvider>
1384
- );
1385
-
1386
- // Wait for useEffect to execute
1387
- await new Promise(resolve => setTimeout(resolve, 10));
1388
-
1389
- expect(consoleSpy).toHaveBeenCalled();
1390
- });
1391
-
1392
- it('does not render when disabled', () => {
1393
- render(
1394
- <PublicPageProvider>
1395
- <PublicPageDebugger enabled={false} />
1396
- </PublicPageProvider>
1397
- );
1398
-
1399
- expect(screen.queryByText('Public Page Debugger')).not.toBeInTheDocument();
1400
- });
1401
- });
1402
- ```
1403
-
1404
- ### Testing PublicPageContextChecker
1405
-
1406
- **Key Principles**:
1407
- - Test immediate context validation
1408
- - Test console error warnings
1409
- - Test architecture guidance
1410
-
1411
- ```typescript
1412
- describe('PublicPageContextChecker', () => {
1413
- it('groups console output with proper labels', async () => {
1414
- const consoleGroupSpy = vi.spyOn(console, 'group');
1415
-
1416
- render(
1417
- <PublicPageProvider>
1418
- <PublicPageContextChecker enabled={true} label="TestChecker" />
1419
- </PublicPageProvider>
1420
- );
1421
-
1422
- await new Promise(resolve => setTimeout(resolve, 10));
1423
-
1424
- expect(consoleGroupSpy).toHaveBeenCalled();
1425
- });
1426
-
1427
- it('displays warning about context issues', () => {
1428
- render(
1429
- <PublicPageProvider>
1430
- <PublicPageContextChecker enabled={true} />
1431
- </PublicPageProvider>
1432
- );
1433
-
1434
- expect(screen.getByText(/If you see ❌ errors in console/)).toBeInTheDocument();
1435
- });
1436
- });
1437
- ```
1438
-
1439
- ### Common Pitfalls
1440
-
1441
- **❌ Don't: Directly set import.meta.env**
1442
- ```typescript
1443
- // ❌ Bad - This doesn't work reliably in tests
1444
- (import.meta as any).env.VITE_SUPABASE_URL = 'http://localhost';
1445
-
1446
- // ✅ Good - Mock createClient instead
1447
- vi.mock('@supabase/supabase-js', () => ({
1448
- createClient: vi.fn(() => mockClient)
1449
- }));
1450
- ```
1451
-
1452
- **❌ Don't: Test internal environment variable values**
1453
- ```typescript
1454
- // ❌ Bad - Testing implementation details
1455
- expect(context.environment.supabaseUrl).toBe('specific-value');
1456
-
1457
- // ✅ Good - Test observable behavior
1458
- expect(console.warn).toHaveBeenCalledWith(
1459
- expect.stringContaining('Missing Supabase environment variables')
1460
- );
1461
- ```
1462
-
1463
- **✅ Do: Test observable behavior**
1464
- - Verify context is provided (not specific values)
1465
- - Verify Supabase client attempts creation
1466
- - Verify error boundary wraps children
1467
- - Verify console warnings are logged when appropriate
1468
-
1469
- ---
1470
-
1471
- ## 🎯 Service Hook Testing Patterns
1472
-
1473
- Service hooks provide access to services with reactive state updates. They follow a simple but critical pattern.
1474
-
1475
- ### Pattern Overview
1476
-
1477
- All service hooks follow the same structure:
1478
- 1. Get context from provider
1479
- 2. Throw if context is missing
1480
- 3. Subscribe to service state changes
1481
- 4. Force re-render on service updates
1482
-
1483
- ### Testing Context Validation
1484
-
1485
- ```typescript
1486
- describe('Service Hooks', () => {
1487
- it('throws error when used outside provider', () => {
1488
- expect(() => {
1489
- renderHook(() => useEventService());
1490
- }).toThrow('useEventService must be used within EventServiceProvider');
1491
- });
1492
-
1493
- it('validates provider requirement consistently', () => {
1494
- const errorMessages = [
1495
- 'useEventService must be used within EventServiceProvider',
1496
- 'useOrganisationService must be used within OrganisationServiceProvider',
1497
- 'useInactivityService must be used within InactivityServiceProvider'
1498
- ];
1499
-
1500
- errorMessages.forEach(msg => {
1501
- expect(msg).toMatch(/must be used within \w+Provider$/);
1502
- });
1503
- });
1504
- });
1505
- ```
1506
-
1507
- ### Testing Subscription Cleanup
1508
-
1509
- ```typescript
1510
- describe('Service Hook Subscriptions', () => {
1511
- it('cleans up subscriptions on unmount', () => {
1512
- const unsubscribe = vi.fn();
1513
- const mockService = {
1514
- subscribe: vi.fn(() => unsubscribe)
1515
- };
1516
-
1517
- const { unmount } = renderHook(() => useEventService(), {
1518
- wrapper: ({ children }) => (
1519
- <EventServiceContext.Provider value={{ eventService: mockService }}>
1520
- {children}
1521
- </EventServiceContext.Provider>
1522
- )
1523
- });
1524
-
1525
- unmount();
1526
-
1527
- expect(unsubscribe).toHaveBeenCalled();
1528
- });
1529
- });
1530
- ```
1531
-
1532
- ### Testing Error Message Consistency
1533
-
1534
- ```typescript
1535
- describe('Service Hook Error Handling', () => {
1536
- it('all hooks use consistent error message pattern', () => {
1537
- const hooks = [
1538
- useEventService,
1539
- useOrganisationService,
1540
- useInactivityService,
1541
- useRBACService,
1542
- useAuthService
1543
- ];
1544
-
1545
- hooks.forEach(hook => {
1546
- expect(() => renderHook(() => hook())).toThrow(
1547
- /must be used within \w+Provider$/
1548
- );
1549
- });
1550
- });
1551
- });
1552
- ```
1553
-
1554
- ### Common Patterns
1555
-
1556
- **Hook Pattern**:
1557
- ```typescript
1558
- export function useEventService(): EventService {
1559
- const context = useContext(EventServiceContext);
1560
-
1561
- if (!context) {
1562
- throw new Error('useEventService must be used within EventServiceProvider');
1563
- }
1564
-
1565
- const [, forceUpdate] = useReducer(x => x + 1, 0);
1566
-
1567
- useEffect(() => {
1568
- return context.eventService.subscribe(() => forceUpdate());
1569
- }, [context.eventService]);
1570
-
1571
- return context.eventService;
1572
- }
1573
- ```
1574
-
1575
- **Test Pattern**:
1576
- ```typescript
1577
- describe('useServiceHook', () => {
1578
- it('throws error when outside provider', () => {
1579
- expect(() => renderHook(() => useService())).toThrow();
1580
- });
1581
-
1582
- it('returns service when inside provider', () => {
1583
- const { result } = renderHook(() => useService(), {
1584
- wrapper: ({ children }) => (
1585
- <ServiceContext.Provider value={{ service: mockService }}>
1586
- {children}
1587
- </ServiceContext.Provider>
1588
- )
1589
- });
1590
-
1591
- expect(result.current).toBe(mockService);
1592
- });
1593
-
1594
- it('subscribes to service updates', () => {
1595
- // Test subscription logic
1596
- });
1597
- });
1598
- ```
1599
-
1600
- ### Benefits
1601
-
1602
- - **Consistent Error Messages**: All hooks provide clear, actionable error messages
1603
- - **Reactivity**: Components re-render when service state changes
1604
- - **Automatic Cleanup**: Subscriptions are cleaned up on unmount
1605
- - **Type Safety**: TypeScript ensures correct service type usage