@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.
- package/dist/{AuthService-Df3IozMG.d.ts → AuthService-DcTI5Ov4.d.ts} +9 -0
- package/dist/{DataTable-FA6EUX5M.js → DataTable-PWBMKMOG.js} +7 -7
- package/dist/{PublicLoadingSpinner-DecuJBX0.d.ts → PublicLoadingSpinner-BQXD1fbO.d.ts} +160 -130
- package/dist/{UnifiedAuthProvider-K2IZAY5F.js → UnifiedAuthProvider-5D3HEQND.js} +4 -4
- package/dist/{UnifiedAuthProvider-B391Aqum.d.ts → UnifiedAuthProvider-BVKmQd9u.d.ts} +4 -0
- package/dist/auth-DReDSLq9.d.ts +16 -0
- package/dist/{chunk-CBSD3BZ3.js → chunk-3RZBKQ5Y.js} +2 -6
- package/dist/{chunk-CBSD3BZ3.js.map → chunk-3RZBKQ5Y.js.map} +1 -1
- package/dist/{chunk-NTW3KGS4.js → chunk-6UHXQH7P.js} +5 -5
- package/dist/{chunk-YVUZWLQG.js → chunk-AQGF5OG7.js} +3 -3
- package/dist/{chunk-CVMVPYAL.js → chunk-BDZUMRBD.js} +3 -5
- package/dist/chunk-BDZUMRBD.js.map +1 -0
- package/dist/{chunk-KAY3K5TP.js → chunk-BNXBJOGL.js} +4 -4
- package/dist/{chunk-I7O3RSMN.js → chunk-CJIZS3UE.js} +1298 -769
- package/dist/chunk-CJIZS3UE.js.map +1 -0
- package/dist/{chunk-S3JKDMD5.js → chunk-CXKMRKRF.js} +4 -4
- package/dist/{chunk-5BN3YGNK.js → chunk-DP5X5ORK.js} +217 -27
- package/dist/chunk-DP5X5ORK.js.map +1 -0
- package/dist/{chunk-ZFLOV3OM.js → chunk-H3P2RGKZ.js} +352 -16
- package/dist/chunk-H3P2RGKZ.js.map +1 -0
- package/dist/{chunk-RIXPZJUB.js → chunk-KTPG5VCH.js} +2 -2
- package/dist/{chunk-WUXCWRL6.js → chunk-XJ2HZOBU.js} +6 -1
- package/dist/chunk-XJ2HZOBU.js.map +1 -0
- package/dist/{chunk-2FQEQUJT.js → chunk-XXVM53P4.js} +4 -4
- package/dist/{chunk-I2VVV5PQ.js → chunk-YY4YYM3E.js} +2 -2
- package/dist/components.d.ts +6 -55
- package/dist/components.js +24 -205
- package/dist/components.js.map +1 -1
- package/dist/{file-reference-9xUOnwyt.d.ts → file-reference-C9isKNPn.d.ts} +67 -2
- package/dist/hooks.js +9 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +152 -26
- package/dist/index.js +64 -194
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +5 -3
- package/dist/providers.js +3 -3
- package/dist/rbac/index.js +8 -8
- package/dist/types.d.ts +2 -1
- package/dist/types.js +3 -3
- package/dist/utils.js +2 -2
- package/docs/DOCUMENTATION_AUDIT.md +6 -6
- package/docs/DOCUMENTATION_STANDARD.md +137 -0
- package/docs/README.md +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +83 -40
- package/docs/api/enums/FileCategory.md +56 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +11 -11
- package/docs/api/interfaces/FileDisplayProps.md +10 -10
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +8 -8
- package/docs/api/interfaces/FileUploadProps.md +137 -42
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +83 -50
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
- package/docs/api/interfaces/UseEventLogoOptions.md +74 -0
- package/docs/api/interfaces/UseEventLogoReturn.md +81 -0
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +6 -6
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +6 -6
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +290 -95
- package/docs/api-reference/components.md +1 -18
- package/docs/api-reference/hooks.md +1 -4
- package/docs/best-practices/testing.md +2 -0
- package/docs/documentation-index.md +1 -1
- package/docs/getting-started/faq.md +1 -1
- package/docs/implementation-guides/file-reference-system.md +592 -58
- package/docs/implementation-guides/file-upload-storage.md +137 -73
- package/docs/rbac/super-admin-guide.md +18 -70
- package/docs/testing/README.md +2 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +674 -0
- package/src/__tests__/helpers/test-utils.tsx +3 -2
- package/src/components/DataTable/__tests__/{DataTable.comprehensive.test.tsx.skip → DataTable.comprehensive.test.tsx} +17 -18
- package/src/components/DataTable/__tests__/{DataTable.test.tsx.skip → DataTable.test.tsx} +14 -22
- package/src/components/DataTable/__tests__/{ssr.strict-mode.test.tsx.skip → ssr.strict-mode.test.tsx} +42 -47
- package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +1 -1
- package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +13 -4
- package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +1 -1
- package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +10 -6
- package/src/components/FileDisplay/FileDisplay.test.tsx +257 -0
- package/src/components/{FileDisplay.tsx → FileDisplay/FileDisplay.tsx} +111 -10
- package/src/components/FileDisplay/index.tsx +4 -0
- package/src/components/FileUpload/FileUpload.test.tsx +171 -621
- package/src/components/FileUpload/FileUpload.tsx +512 -168
- package/src/components/FileUpload/index.tsx +4 -0
- package/src/components/Progress/Progress.test.tsx +38 -0
- package/src/components/PublicLayout/EventLogo.tsx +6 -4
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/SessionRestorationLoader.tsx +48 -0
- package/src/components/Toast/Toast.tsx +13 -8
- package/src/components/index.ts +16 -16
- package/src/hooks/__tests__/ServiceHooks.test.tsx +615 -0
- package/src/hooks/public/usePublicEventLogo.ts +16 -20
- package/src/hooks/useEventLogo.ts +316 -0
- package/src/hooks/useEvents.ts +0 -5
- package/src/hooks/useFileReference.test.ts +659 -0
- package/src/hooks/useFileReference.ts +207 -3
- package/src/hooks/useSessionRestoration.ts +64 -0
- package/src/index.ts +17 -5
- package/src/providers/{UnifiedAuthProvider.test.simple.tsx → UnifiedAuthProvider.smoke.test.tsx} +81 -60
- package/src/providers/services/AuthServiceProvider.tsx +27 -3
- package/src/providers/services/UnifiedAuthProvider.tsx +34 -5
- package/src/rbac/{engine.test.simple.ts → RBACEngine.smoke.test.ts} +17 -12
- package/src/services/AuthService.ts +142 -20
- package/src/services/EventService.ts +0 -4
- package/src/types/auth.ts +15 -0
- package/src/types/file-reference.ts +73 -1
- package/src/types/index.ts +1 -0
- package/src/utils/__tests__/organisationContext.unit.test.ts +2 -4
- package/src/utils/appNameResolver.simple.test.ts +99 -29
- package/src/utils/file-reference.test.ts +535 -0
- package/src/utils/file-reference.ts +200 -30
- package/src/utils/organisationContext.test.ts +5 -19
- package/src/utils/organisationContext.ts +3 -5
- package/src/utils/storage/README.md +269 -262
- package/src/utils/storage/config.ts +9 -0
- package/src/utils/storage/helpers.test.ts +631 -0
- package/src/utils/storage/helpers.ts +112 -14
- package/src/utils/storage/index.ts +3 -0
- package/src/validation/__tests__/sanitization.unit.test.ts +1 -1
- package/src/validation/__tests__/schemaUtils.unit.test.ts +1 -1
- package/src/validation/__tests__/user.unit.test.ts +1 -1
- package/dist/chunk-5BN3YGNK.js.map +0 -1
- package/dist/chunk-CVMVPYAL.js.map +0 -1
- package/dist/chunk-I7O3RSMN.js.map +0 -1
- package/dist/chunk-WUXCWRL6.js.map +0 -1
- package/dist/chunk-ZFLOV3OM.js.map +0 -1
- package/docs/CONTENT_AUDIT_REPORT.md +0 -253
- package/docs/STYLE_GUIDE.md +0 -37
- package/examples/RBAC/__tests__/PermissionExample.test.tsx +0 -150
- package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +0 -159
- package/src/__tests__/TEST_GUIDE_CURSOR.md +0 -1605
- package/src/__tests__/TEST_GUIDE_HUMAN.md +0 -103
- package/src/components/FileUpload/FileUpload.example.tsx +0 -218
- package/src/components/FileUpload/index.ts +0 -6
- package/src/components/FileUpload.tsx +0 -176
- package/src/components/Progress/index.ts +0 -3
- package/src/components/PublicLayout/__tests__/EventLogo.test.tsx +0 -666
- package/src/components/SuperAdminGuard.tsx +0 -116
- package/src/components/__tests__/FileDisplay.test.tsx +0 -575
- package/src/components/__tests__/FileUpload.test.tsx +0 -446
- package/src/components/__tests__/SuperAdminGuard.test.tsx +0 -627
- package/src/components/examples/PermissionExample.tsx +0 -173
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +0 -583
- package/src/hooks/__tests__/usePublicEventLogo.unit.test.ts +0 -640
- package/src/types/__tests__/file-reference.test.ts +0 -447
- package/src/utils/__tests__/file-reference.test.ts +0 -383
- /package/dist/{DataTable-FA6EUX5M.js.map → DataTable-PWBMKMOG.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-K2IZAY5F.js.map → UnifiedAuthProvider-5D3HEQND.js.map} +0 -0
- /package/dist/{chunk-NTW3KGS4.js.map → chunk-6UHXQH7P.js.map} +0 -0
- /package/dist/{chunk-YVUZWLQG.js.map → chunk-AQGF5OG7.js.map} +0 -0
- /package/dist/{chunk-KAY3K5TP.js.map → chunk-BNXBJOGL.js.map} +0 -0
- /package/dist/{chunk-S3JKDMD5.js.map → chunk-CXKMRKRF.js.map} +0 -0
- /package/dist/{chunk-RIXPZJUB.js.map → chunk-KTPG5VCH.js.map} +0 -0
- /package/dist/{chunk-2FQEQUJT.js.map → chunk-XXVM53P4.js.map} +0 -0
- /package/dist/{chunk-I2VVV5PQ.js.map → chunk-YY4YYM3E.js.map} +0 -0
- /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
|