@jmruthers/pace-core 0.5.187 → 0.5.189

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 (209) hide show
  1. package/dist/{DataTable-K3RJRSOX.js → DataTable-GUFUNZ3N.js} +5 -5
  2. package/dist/{PublicPageProvider-DrLDztHt.d.ts → PublicPageProvider-B8HaLe69.d.ts} +47 -17
  3. package/dist/{UnifiedAuthProvider-B76OWOAT.js → UnifiedAuthProvider-643PUAIM.js} +2 -2
  4. package/dist/{chunk-FMTK4XNN.js → chunk-2UUZZJFT.js} +3 -3
  5. package/dist/{chunk-3IC5WCMO.js → chunk-3GOZZZYH.js} +3 -3
  6. package/dist/{chunk-ULX5FYEM.js → chunk-DDM4CCYT.js} +3 -3
  7. package/dist/{chunk-K2JGDXGU.js → chunk-E7UAOUMY.js} +2 -2
  8. package/dist/{chunk-T6ZJVI3A.js → chunk-IM4QE42D.js} +4 -4
  9. package/dist/{chunk-3NFNJOO7.js → chunk-MX64ZF6I.js} +4 -4
  10. package/dist/{chunk-C4OYJOV4.js → chunk-UCQSRW7Z.js} +829 -829
  11. package/dist/chunk-UCQSRW7Z.js.map +1 -0
  12. package/dist/{chunk-WK2Y6TGA.js → chunk-VGZZXKBR.js} +3 -3
  13. package/dist/chunk-VGZZXKBR.js.map +1 -0
  14. package/dist/{chunk-LBBUPSSC.js → chunk-YGPFYGA6.js} +3760 -3692
  15. package/dist/chunk-YGPFYGA6.js.map +1 -0
  16. package/dist/components.d.ts +1 -2
  17. package/dist/components.js +6 -10
  18. package/dist/components.js.map +1 -1
  19. package/dist/hooks.js +5 -5
  20. package/dist/index.d.ts +1 -2
  21. package/dist/index.js +9 -13
  22. package/dist/index.js.map +1 -1
  23. package/dist/providers.js +1 -1
  24. package/dist/rbac/index.js +5 -5
  25. package/dist/utils.js +1 -1
  26. package/docs/api/classes/ColumnFactory.md +1 -1
  27. package/docs/api/classes/ErrorBoundary.md +1 -1
  28. package/docs/api/classes/InvalidScopeError.md +1 -1
  29. package/docs/api/classes/Logger.md +1 -1
  30. package/docs/api/classes/MissingUserContextError.md +1 -1
  31. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  32. package/docs/api/classes/PermissionDeniedError.md +1 -1
  33. package/docs/api/classes/RBACAuditManager.md +1 -1
  34. package/docs/api/classes/RBACCache.md +1 -1
  35. package/docs/api/classes/RBACEngine.md +1 -1
  36. package/docs/api/classes/RBACError.md +1 -1
  37. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  38. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  39. package/docs/api/classes/StorageUtils.md +1 -1
  40. package/docs/api/enums/FileCategory.md +1 -1
  41. package/docs/api/enums/LogLevel.md +1 -1
  42. package/docs/api/enums/RBACErrorCode.md +1 -1
  43. package/docs/api/enums/RPCFunction.md +1 -1
  44. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  45. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  46. package/docs/api/interfaces/AggregateConfig.md +1 -1
  47. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  48. package/docs/api/interfaces/AvatarProps.md +128 -0
  49. package/docs/api/interfaces/BadgeProps.md +1 -1
  50. package/docs/api/interfaces/ButtonProps.md +1 -1
  51. package/docs/api/interfaces/CalendarProps.md +1 -1
  52. package/docs/api/interfaces/CardProps.md +1 -1
  53. package/docs/api/interfaces/ColorPalette.md +1 -1
  54. package/docs/api/interfaces/ColorShade.md +1 -1
  55. package/docs/api/interfaces/ComplianceResult.md +1 -1
  56. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  57. package/docs/api/interfaces/DataRecord.md +1 -1
  58. package/docs/api/interfaces/DataTableAction.md +1 -1
  59. package/docs/api/interfaces/DataTableColumn.md +1 -1
  60. package/docs/api/interfaces/DataTableProps.md +1 -1
  61. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  62. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  63. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  64. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  65. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  66. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  67. package/docs/api/interfaces/ExportColumn.md +1 -1
  68. package/docs/api/interfaces/ExportOptions.md +1 -1
  69. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  70. package/docs/api/interfaces/FileMetadata.md +1 -1
  71. package/docs/api/interfaces/FileReference.md +1 -1
  72. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  73. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  74. package/docs/api/interfaces/FileUploadProps.md +1 -1
  75. package/docs/api/interfaces/FooterProps.md +1 -1
  76. package/docs/api/interfaces/FormFieldProps.md +1 -1
  77. package/docs/api/interfaces/FormProps.md +1 -1
  78. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  79. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  80. package/docs/api/interfaces/InputProps.md +1 -1
  81. package/docs/api/interfaces/LabelProps.md +1 -1
  82. package/docs/api/interfaces/LoggerConfig.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/ParsedAddress.md +1 -1
  103. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  104. package/docs/api/interfaces/ProgressProps.md +1 -1
  105. package/docs/api/interfaces/ProtectedRouteProps.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/QuickFix.md +1 -1
  110. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  111. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  112. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  113. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  114. package/docs/api/interfaces/RBACConfig.md +1 -1
  115. package/docs/api/interfaces/RBACContext.md +1 -1
  116. package/docs/api/interfaces/RBACLogger.md +1 -1
  117. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  118. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  119. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  120. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  121. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  122. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  123. package/docs/api/interfaces/RBACResult.md +1 -1
  124. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  125. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  126. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  127. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  128. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  129. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  130. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  131. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  132. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  133. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  134. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  135. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  136. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  137. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  138. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  139. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  140. package/docs/api/interfaces/RouteConfig.md +1 -1
  141. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  142. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  143. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  144. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  145. package/docs/api/interfaces/SetupIssue.md +1 -1
  146. package/docs/api/interfaces/StorageConfig.md +1 -1
  147. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  148. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  149. package/docs/api/interfaces/StorageListOptions.md +1 -1
  150. package/docs/api/interfaces/StorageListResult.md +1 -1
  151. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  152. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  153. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  154. package/docs/api/interfaces/StyleImport.md +1 -1
  155. package/docs/api/interfaces/SwitchProps.md +1 -1
  156. package/docs/api/interfaces/TabsContentProps.md +1 -1
  157. package/docs/api/interfaces/TabsListProps.md +1 -1
  158. package/docs/api/interfaces/TabsProps.md +1 -1
  159. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  160. package/docs/api/interfaces/TextareaProps.md +1 -1
  161. package/docs/api/interfaces/ToastActionElement.md +1 -1
  162. package/docs/api/interfaces/ToastProps.md +1 -1
  163. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  164. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  165. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  166. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  167. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  168. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  169. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  170. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  171. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  172. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  173. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  174. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  175. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  176. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  177. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  178. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  179. package/docs/api/interfaces/UserEventAccess.md +1 -1
  180. package/docs/api/interfaces/UserMenuProps.md +1 -1
  181. package/docs/api/interfaces/UserProfile.md +1 -1
  182. package/docs/api/modules.md +6 -45
  183. package/docs/api-reference/components.md +57 -22
  184. package/docs/getting-started/examples/README.md +2 -2
  185. package/docs/implementation-guides/public-pages.md +140 -1230
  186. package/docs/standards/05-security-standard.md +3 -1
  187. package/docs/standards/07-rbac-and-rls-standard.md +356 -0
  188. package/package.json +1 -2
  189. package/src/__tests__/public-recipe-view.test.ts +199 -0
  190. package/src/__tests__/rls-policies.test.ts +333 -0
  191. package/src/components/Avatar/Avatar.test.tsx +183 -209
  192. package/src/components/Avatar/Avatar.tsx +179 -53
  193. package/src/components/Avatar/index.ts +1 -1
  194. package/src/components/UserMenu/UserMenu.test.tsx +7 -9
  195. package/src/components/UserMenu/UserMenu.tsx +7 -5
  196. package/src/components/index.ts +2 -1
  197. package/src/index.ts +2 -1
  198. package/src/services/OrganisationService.ts +5 -4
  199. package/dist/chunk-C4OYJOV4.js.map +0 -1
  200. package/dist/chunk-LBBUPSSC.js.map +0 -1
  201. package/dist/chunk-WK2Y6TGA.js.map +0 -1
  202. /package/dist/{DataTable-K3RJRSOX.js.map → DataTable-GUFUNZ3N.js.map} +0 -0
  203. /package/dist/{UnifiedAuthProvider-B76OWOAT.js.map → UnifiedAuthProvider-643PUAIM.js.map} +0 -0
  204. /package/dist/{chunk-FMTK4XNN.js.map → chunk-2UUZZJFT.js.map} +0 -0
  205. /package/dist/{chunk-3IC5WCMO.js.map → chunk-3GOZZZYH.js.map} +0 -0
  206. /package/dist/{chunk-ULX5FYEM.js.map → chunk-DDM4CCYT.js.map} +0 -0
  207. /package/dist/{chunk-K2JGDXGU.js.map → chunk-E7UAOUMY.js.map} +0 -0
  208. /package/dist/{chunk-T6ZJVI3A.js.map → chunk-IM4QE42D.js.map} +0 -0
  209. /package/dist/{chunk-3NFNJOO7.js.map → chunk-MX64ZF6I.js.map} +0 -0
@@ -1,38 +1,50 @@
1
1
  /**
2
2
  * @file Avatar Component Tests
3
- * @description Comprehensive tests for Avatar, AvatarImage, and AvatarFallback components
3
+ * @description Comprehensive tests for Avatar component
4
4
  */
5
5
 
6
6
  import React from 'react';
7
7
  import { screen, waitFor } from '@testing-library/react';
8
8
  import { describe, it, expect, vi, beforeEach } from 'vitest';
9
- import { Avatar, AvatarImage, AvatarFallback } from './Avatar';
9
+ import { Avatar } from './Avatar';
10
+ import { FileCategory } from '../../types/file-reference';
10
11
  import { renderWithProviders, userEvent } from '../../__tests__/helpers/test-utils';
11
12
 
13
+ // Size classes for testing
14
+ const sizeClasses = {
15
+ xs: 'h-4 w-4 text-xs',
16
+ sm: 'h-6 w-6 text-sm',
17
+ md: 'h-10 w-10 text-base',
18
+ lg: 'h-12 w-12 text-lg',
19
+ xl: 'h-16 w-16 text-xl',
20
+ '2xl': 'h-20 w-20 text-2xl'
21
+ };
22
+
12
23
  // Helper function to get the avatar container element
13
24
  const getAvatarContainer = (fallbackText: string) => {
14
- return screen.getByText(fallbackText).closest('span')?.parentElement;
25
+ return screen.getByText(fallbackText).closest('div');
15
26
  };
16
27
 
17
28
  describe('Avatar Component', () => {
18
- describe('Rendering', () => {
19
- it('renders with default props', () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ });
32
+
33
+ describe('Rendering', () => {
34
+ it('renders with fallback only', () => {
20
35
  renderWithProviders(
21
- <Avatar>
22
- <AvatarFallback>AB</AvatarFallback>
23
- </Avatar>
36
+ <Avatar fallback="AB" />
24
37
  );
25
38
 
26
39
  const avatar = getAvatarContainer('AB');
27
40
  expect(avatar).toBeInTheDocument();
28
41
  expect(avatar).toHaveClass('relative', 'flex', 'h-10', 'w-10', 'shrink-0', 'overflow-hidden', 'rounded-full');
42
+ expect(screen.getByText('AB')).toBeInTheDocument();
29
43
  });
30
44
 
31
45
  it('renders with custom className', () => {
32
46
  renderWithProviders(
33
- <Avatar className="custom-avatar-class">
34
- <AvatarFallback>JD</AvatarFallback>
35
- </Avatar>
47
+ <Avatar fallback="JD" className="custom-avatar-class" />
36
48
  );
37
49
 
38
50
  const avatar = getAvatarContainer('JD');
@@ -41,32 +53,35 @@ describe('Avatar Component', () => {
41
53
 
42
54
  it('renders with custom size via className', () => {
43
55
  renderWithProviders(
44
- <Avatar className="h-16 w-16">
45
- <AvatarFallback>LG</AvatarFallback>
46
- </Avatar>
56
+ <Avatar fallback="LG" className="h-16 w-16" />
47
57
  );
48
58
 
49
59
  const avatar = getAvatarContainer('LG');
50
60
  expect(avatar).toHaveClass('h-16', 'w-16');
51
61
  });
52
62
 
63
+ it('renders with size prop', () => {
64
+ renderWithProviders(
65
+ <Avatar fallback="SM" size="sm" />
66
+ );
67
+
68
+ const avatar = getAvatarContainer('SM');
69
+ expect(avatar).toHaveClass('h-6', 'w-6', 'text-sm');
70
+ });
71
+
53
72
  it('forwards ref correctly', () => {
54
- const ref = React.createRef<HTMLSpanElement>();
73
+ const ref = React.createRef<HTMLDivElement>();
55
74
 
56
75
  renderWithProviders(
57
- <Avatar ref={ref}>
58
- <AvatarFallback>RF</AvatarFallback>
59
- </Avatar>
76
+ <Avatar ref={ref} fallback="RF" />
60
77
  );
61
78
 
62
- expect(ref.current).toBeInstanceOf(HTMLSpanElement);
79
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
63
80
  });
64
81
 
65
82
  it('accepts HTML attributes', () => {
66
83
  renderWithProviders(
67
- <Avatar data-testid="avatar" aria-label="User avatar">
68
- <AvatarFallback>AT</AvatarFallback>
69
- </Avatar>
84
+ <Avatar data-testid="avatar" aria-label="User avatar" fallback="AT" />
70
85
  );
71
86
 
72
87
  const avatar = screen.getByTestId('avatar');
@@ -74,138 +89,126 @@ describe('Avatar Component', () => {
74
89
  });
75
90
  });
76
91
 
77
- describe('AvatarImage Component', () => {
78
- it('renders with src and alt attributes when image loads', async () => {
79
- // Mock successful image load
80
- const mockImage = {
81
- onload: null as (() => void) | null,
82
- onerror: null as (() => void) | null,
83
- src: '',
84
- alt: '',
85
- addEventListener: vi.fn(),
86
- removeEventListener: vi.fn(),
87
- };
88
-
89
- Object.defineProperty(global, 'Image', {
90
- value: vi.fn(() => {
91
- setTimeout(() => {
92
- if (mockImage.onload) mockImage.onload();
93
- }, 0);
94
- return mockImage;
95
- }),
96
- writable: true,
97
- });
92
+ describe('Direct URL Approach', () => {
93
+ it('renders image with src and alt attributes', () => {
94
+ renderWithProviders(
95
+ <Avatar src="/user.jpg" alt="User profile" fallback="UI" />
96
+ );
97
+
98
+ const image = screen.getByAltText('User profile');
99
+ expect(image).toBeInTheDocument();
100
+ expect(image).toHaveAttribute('src', '/user.jpg');
101
+ expect(image).toHaveAttribute('alt', 'User profile');
102
+ expect(image.tagName).toBe('IMG');
103
+ });
98
104
 
105
+ it('shows fallback when image fails to load', async () => {
99
106
  renderWithProviders(
100
- <Avatar>
101
- <AvatarImage src="/user.jpg" alt="User profile" />
102
- <AvatarFallback>UI</AvatarFallback>
103
- </Avatar>
107
+ <Avatar src="/broken-image.jpg" alt="User" fallback="FL" />
104
108
  );
105
109
 
106
- // Initially shows fallback
107
- expect(screen.getByText('UI')).toBeInTheDocument();
110
+ const image = screen.getByAltText('User');
111
+ expect(image).toBeInTheDocument();
112
+
113
+ // Simulate image error
114
+ const errorEvent = new Event('error');
115
+ image.dispatchEvent(errorEvent);
108
116
 
109
- // Wait for image to load
110
117
  await waitFor(() => {
111
- const image = screen.queryByAltText('User profile');
112
- if (image) {
113
- expect(image).toHaveAttribute('src', '/user.jpg');
114
- expect(image).toHaveAttribute('alt', 'User profile');
115
- }
118
+ expect(screen.getByText('FL')).toBeInTheDocument();
116
119
  });
117
120
  });
118
121
 
119
- it('shows fallback when image fails to load', () => {
122
+ it('uses fallback as alt text when alt not provided', () => {
120
123
  renderWithProviders(
121
- <Avatar>
122
- <AvatarImage src="/broken-image.jpg" alt="User" />
123
- <AvatarFallback>FL</AvatarFallback>
124
- </Avatar>
124
+ <Avatar src="/user.jpg" fallback="JD" />
125
125
  );
126
126
 
127
- // Should show fallback when image fails
128
- expect(screen.getByText('FL')).toBeInTheDocument();
129
- expect(screen.queryByAltText('User')).not.toBeInTheDocument();
127
+ const image = screen.getByAltText('JD');
128
+ expect(image).toBeInTheDocument();
130
129
  });
131
130
 
132
- it('handles missing src gracefully', () => {
131
+ it('applies image classes correctly', () => {
133
132
  renderWithProviders(
134
- <Avatar>
135
- <AvatarImage alt="User" />
136
- <AvatarFallback>MS</AvatarFallback>
137
- </Avatar>
133
+ <Avatar src="/user.jpg" alt="User" fallback="IC" className="custom-img-class" />
138
134
  );
139
135
 
140
- // Should show fallback when image src is missing
141
- expect(screen.getByText('MS')).toBeInTheDocument();
136
+ const image = screen.getByAltText('User');
137
+ expect(image).toHaveClass('object-cover', 'h-full', 'w-full', 'custom-img-class');
142
138
  });
139
+ });
143
140
 
144
- it('forwards ref correctly', () => {
145
- const ref = React.createRef<HTMLImageElement>();
146
-
147
- renderWithProviders(
148
- <Avatar>
149
- <AvatarImage ref={ref} src="/user.jpg" alt="User" />
150
- <AvatarFallback>RF</AvatarFallback>
151
- </Avatar>
141
+ describe('File Reference Props Approach', () => {
142
+ it('renders FileDisplay when file props provided', () => {
143
+ const { container } = renderWithProviders(
144
+ <Avatar
145
+ table_name="user_profiles"
146
+ record_id="user-123"
147
+ organisation_id="org-123"
148
+ category={FileCategory.PROFILE_PHOTOS}
149
+ fallback="FP"
150
+ />
152
151
  );
153
152
 
154
- // Ref might be null if image doesn't load immediately, which is expected behavior
155
- // The important thing is that the component renders without errors
156
- expect(screen.getByText('RF')).toBeInTheDocument();
153
+ // FileDisplay should be rendered (it will show fallback if no file exists)
154
+ const avatar = getAvatarContainer('FP');
155
+ expect(avatar).toBeInTheDocument();
157
156
  });
158
157
 
159
- it('accepts HTML attributes', () => {
158
+ it('requires all file props for file reference approach', () => {
160
159
  renderWithProviders(
161
- <Avatar>
162
- <AvatarImage
163
- src="/user.jpg"
164
- alt="User"
165
- data-testid="avatar-image"
166
- loading="lazy"
167
- />
168
- <AvatarFallback>AT</AvatarFallback>
169
- </Avatar>
170
- );
171
-
172
- // Check if image element exists with attributes
173
- const image = screen.queryByTestId('avatar-image');
174
- if (image) {
175
- expect(image).toHaveAttribute('loading', 'lazy');
176
- }
160
+ <Avatar
161
+ table_name="user_profiles"
162
+ record_id="user-123"
163
+ organisation_id="org-123"
164
+ // category missing
165
+ fallback="MP"
166
+ />
167
+ );
168
+
169
+ // Should show fallback when props incomplete
170
+ expect(screen.getByText('MP')).toBeInTheDocument();
177
171
  });
178
172
  });
179
173
 
180
- describe('AvatarFallback Component', () => {
181
- it('renders with text content', () => {
174
+ describe('File ID Approach', () => {
175
+ it('shows fallback when fileId provided but no supabase', () => {
182
176
  renderWithProviders(
183
- <Avatar>
184
- <AvatarFallback>AB</AvatarFallback>
185
- </Avatar>
177
+ <Avatar
178
+ fileId="file-123"
179
+ organisation_id="org-123"
180
+ fallback="FI"
181
+ />
186
182
  );
187
183
 
188
- const fallback = screen.getByText('AB');
189
- expect(fallback).toBeInTheDocument();
190
- expect(fallback).toHaveClass('flex', 'h-full', 'w-full', 'items-center', 'justify-center', 'rounded-full', 'text-sec-50', 'bg-sec-500');
184
+ // Should show fallback when supabase not available
185
+ expect(screen.getByText('FI')).toBeInTheDocument();
191
186
  });
187
+ });
192
188
 
193
- it('renders with custom className', () => {
189
+ describe('Fallback Display', () => {
190
+ it('renders fallback with correct styling', () => {
194
191
  renderWithProviders(
195
- <Avatar>
196
- <AvatarFallback className="custom-fallback-class">CF</AvatarFallback>
197
- </Avatar>
192
+ <Avatar fallback="AB" />
198
193
  );
199
194
 
200
- const fallback = screen.getByText('CF');
201
- expect(fallback).toHaveClass('custom-fallback-class');
195
+ const fallback = screen.getByText('AB');
196
+ expect(fallback).toBeInTheDocument();
197
+ expect(fallback).toHaveClass(
198
+ 'flex',
199
+ 'h-full',
200
+ 'w-full',
201
+ 'items-center',
202
+ 'justify-center',
203
+ 'rounded-full',
204
+ 'text-sec-50',
205
+ 'bg-sec-500'
206
+ );
202
207
  });
203
208
 
204
209
  it('renders with initials', () => {
205
210
  renderWithProviders(
206
- <Avatar>
207
- <AvatarFallback>John Doe</AvatarFallback>
208
- </Avatar>
211
+ <Avatar fallback="John Doe" />
209
212
  );
210
213
 
211
214
  expect(screen.getByText('John Doe')).toBeInTheDocument();
@@ -213,90 +216,49 @@ describe('Avatar Component', () => {
213
216
 
214
217
  it('renders with single character', () => {
215
218
  renderWithProviders(
216
- <Avatar>
217
- <AvatarFallback>J</AvatarFallback>
218
- </Avatar>
219
+ <Avatar fallback="J" />
219
220
  );
220
221
 
221
222
  expect(screen.getByText('J')).toBeInTheDocument();
222
223
  });
223
224
 
224
- it('forwards ref correctly', () => {
225
- const ref = React.createRef<HTMLSpanElement>();
226
-
225
+ it('has aria-label on fallback', () => {
227
226
  renderWithProviders(
228
- <Avatar>
229
- <AvatarFallback ref={ref}>RF</AvatarFallback>
230
- </Avatar>
227
+ <Avatar fallback="AB" alt="User avatar" />
231
228
  );
232
229
 
233
- expect(ref.current).toBeInstanceOf(HTMLSpanElement);
230
+ const fallback = screen.getByText('AB');
231
+ expect(fallback).toHaveAttribute('aria-label', 'User avatar');
234
232
  });
235
233
 
236
- it('accepts HTML attributes', () => {
234
+ it('uses fallback as aria-label when alt not provided', () => {
237
235
  renderWithProviders(
238
- <Avatar>
239
- <AvatarFallback data-testid="fallback" aria-label="User initials">
240
- AT
241
- </AvatarFallback>
242
- </Avatar>
236
+ <Avatar fallback="JD" />
243
237
  );
244
238
 
245
- const fallback = screen.getByTestId('fallback');
246
- expect(fallback).toHaveAttribute('aria-label', 'User initials');
239
+ const fallback = screen.getByText('JD');
240
+ expect(fallback).toHaveAttribute('aria-label', 'JD');
247
241
  });
248
242
  });
249
243
 
250
244
  describe('Composition', () => {
251
- it('works with fallback only', () => {
252
- renderWithProviders(
253
- <Avatar>
254
- <AvatarFallback>JD</AvatarFallback>
255
- </Avatar>
256
- );
257
-
258
- const avatar = getAvatarContainer('JD');
259
- expect(avatar).toBeInTheDocument();
260
- expect(screen.getByText('JD')).toBeInTheDocument();
261
- });
262
-
263
245
  it('works with multiple avatars', () => {
264
246
  renderWithProviders(
265
247
  <div>
266
- <Avatar>
267
- <AvatarFallback>AB</AvatarFallback>
268
- </Avatar>
269
- <Avatar>
270
- <AvatarFallback>CD</AvatarFallback>
271
- </Avatar>
248
+ <Avatar fallback="AB" />
249
+ <Avatar fallback="CD" />
272
250
  </div>
273
251
  );
274
252
 
275
253
  expect(screen.getByText('AB')).toBeInTheDocument();
276
254
  expect(screen.getByText('CD')).toBeInTheDocument();
277
255
  });
278
-
279
- it('works with complex fallback content', () => {
280
- renderWithProviders(
281
- <Avatar>
282
- <AvatarFallback>
283
- <span>👤</span>
284
- <span>User</span>
285
- </AvatarFallback>
286
- </Avatar>
287
- );
288
-
289
- expect(screen.getByText('👤')).toBeInTheDocument();
290
- expect(screen.getByText('User')).toBeInTheDocument();
291
- });
292
256
  });
293
257
 
294
258
  describe('Accessibility', () => {
295
259
  it('supports screen readers with fallback', () => {
296
260
  renderWithProviders(
297
- <Avatar>
298
- <AvatarFallback>John Doe</AvatarFallback>
299
- </Avatar>
261
+ <Avatar fallback="John Doe" />
300
262
  );
301
263
 
302
264
  const avatar = getAvatarContainer('John Doe');
@@ -306,9 +268,7 @@ describe('Avatar Component', () => {
306
268
 
307
269
  it('maintains focus management', () => {
308
270
  renderWithProviders(
309
- <Avatar tabIndex={0}>
310
- <AvatarFallback>FM</AvatarFallback>
311
- </Avatar>
271
+ <Avatar tabIndex={0} fallback="FM" />
312
272
  );
313
273
 
314
274
  const avatar = getAvatarContainer('FM');
@@ -319,9 +279,7 @@ describe('Avatar Component', () => {
319
279
  const user = userEvent.setup();
320
280
 
321
281
  renderWithProviders(
322
- <Avatar tabIndex={0}>
323
- <AvatarFallback>KN</AvatarFallback>
324
- </Avatar>
282
+ <Avatar tabIndex={0} fallback="KN" />
325
283
  );
326
284
 
327
285
  const avatar = getAvatarContainer('KN');
@@ -339,22 +297,17 @@ describe('Avatar Component', () => {
339
297
  describe('Edge Cases', () => {
340
298
  it('handles empty fallback content', () => {
341
299
  renderWithProviders(
342
- <Avatar>
343
- <AvatarFallback></AvatarFallback>
344
- </Avatar>
300
+ <Avatar fallback="" />
345
301
  );
346
302
 
347
- // For empty content, we can check that the avatar container exists
348
- // by looking for the specific class structure
303
+ // Avatar container should exist even with empty fallback
349
304
  const avatar = document.querySelector('.relative.flex.h-10.w-10');
350
305
  expect(avatar).toBeInTheDocument();
351
306
  });
352
307
 
353
308
  it('handles very long fallback text', () => {
354
309
  renderWithProviders(
355
- <Avatar>
356
- <AvatarFallback>Very Long Username That Might Overflow</AvatarFallback>
357
- </Avatar>
310
+ <Avatar fallback="Very Long Username That Might Overflow" />
358
311
  );
359
312
 
360
313
  expect(screen.getByText('Very Long Username That Might Overflow')).toBeInTheDocument();
@@ -362,9 +315,7 @@ describe('Avatar Component', () => {
362
315
 
363
316
  it('handles special characters in fallback', () => {
364
317
  renderWithProviders(
365
- <Avatar>
366
- <AvatarFallback>@#$%</AvatarFallback>
367
- </Avatar>
318
+ <Avatar fallback="@#$%" />
368
319
  );
369
320
 
370
321
  expect(screen.getByText('@#$%')).toBeInTheDocument();
@@ -372,23 +323,37 @@ describe('Avatar Component', () => {
372
323
 
373
324
  it('handles numeric fallback content', () => {
374
325
  renderWithProviders(
375
- <Avatar>
376
- <AvatarFallback>123</AvatarFallback>
377
- </Avatar>
326
+ <Avatar fallback="123" />
378
327
  );
379
328
 
380
329
  expect(screen.getByText('123')).toBeInTheDocument();
381
330
  });
331
+
332
+ it('prioritizes direct URL over file props', () => {
333
+ renderWithProviders(
334
+ <Avatar
335
+ src="/user.jpg"
336
+ alt="User"
337
+ table_name="user_profiles"
338
+ record_id="user-123"
339
+ organisation_id="org-123"
340
+ category={FileCategory.PROFILE_PHOTOS}
341
+ fallback="PR"
342
+ />
343
+ );
344
+
345
+ // Should show image, not FileDisplay
346
+ const image = screen.getByAltText('User');
347
+ expect(image).toBeInTheDocument();
348
+ expect(image).toHaveAttribute('src', '/user.jpg');
349
+ });
382
350
  });
383
351
 
384
352
  describe('Integration', () => {
385
353
  it('works within user profile components', () => {
386
354
  renderWithProviders(
387
355
  <div className="user-profile">
388
- <Avatar>
389
- <AvatarImage src="/user.jpg" alt="Profile" />
390
- <AvatarFallback>UP</AvatarFallback>
391
- </Avatar>
356
+ <Avatar src="/user.jpg" alt="Profile" fallback="UP" />
392
357
  <div>
393
358
  <h3>User Name</h3>
394
359
  <p>User description</p>
@@ -396,7 +361,7 @@ describe('Avatar Component', () => {
396
361
  </div>
397
362
  );
398
363
 
399
- expect(screen.getByText('UP')).toBeInTheDocument();
364
+ expect(screen.getByAltText('Profile')).toBeInTheDocument();
400
365
  expect(screen.getByText('User Name')).toBeInTheDocument();
401
366
  expect(screen.getByText('User description')).toBeInTheDocument();
402
367
  });
@@ -404,9 +369,7 @@ describe('Avatar Component', () => {
404
369
  it('works within navigation menus', () => {
405
370
  renderWithProviders(
406
371
  <nav>
407
- <Avatar>
408
- <AvatarFallback>NM</AvatarFallback>
409
- </Avatar>
372
+ <Avatar fallback="NM" />
410
373
  <ul>
411
374
  <li>Dashboard</li>
412
375
  <li>Settings</li>
@@ -424,9 +387,7 @@ describe('Avatar Component', () => {
424
387
  const handleClick = vi.fn();
425
388
 
426
389
  renderWithProviders(
427
- <Avatar onClick={handleClick} tabIndex={0}>
428
- <AvatarFallback>CH</AvatarFallback>
429
- </Avatar>
390
+ <Avatar onClick={handleClick} tabIndex={0} fallback="CH" />
430
391
  );
431
392
 
432
393
  const avatar = getAvatarContainer('CH');
@@ -440,9 +401,7 @@ describe('Avatar Component', () => {
440
401
  describe('Styling and Layout', () => {
441
402
  it('applies correct default styling', () => {
442
403
  renderWithProviders(
443
- <Avatar>
444
- <AvatarFallback>ST</AvatarFallback>
445
- </Avatar>
404
+ <Avatar fallback="ST" />
446
405
  );
447
406
 
448
407
  const avatar = getAvatarContainer('ST');
@@ -459,9 +418,7 @@ describe('Avatar Component', () => {
459
418
 
460
419
  it('applies correct fallback styling', () => {
461
420
  renderWithProviders(
462
- <Avatar>
463
- <AvatarFallback>FS</AvatarFallback>
464
- </Avatar>
421
+ <Avatar fallback="FS" />
465
422
  );
466
423
 
467
424
  const fallback = screen.getByText('FS');
@@ -479,13 +436,30 @@ describe('Avatar Component', () => {
479
436
 
480
437
  it('handles custom size overrides', () => {
481
438
  renderWithProviders(
482
- <Avatar className="h-20 w-20">
483
- <AvatarFallback>CS</AvatarFallback>
484
- </Avatar>
439
+ <Avatar className="h-20 w-20" fallback="CS" />
485
440
  );
486
441
 
487
442
  const avatar = getAvatarContainer('CS');
488
443
  expect(avatar).toHaveClass('h-20', 'w-20');
489
444
  });
445
+
446
+ it('applies size variants correctly', () => {
447
+ const sizes: Array<'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'> = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
448
+
449
+ sizes.forEach(size => {
450
+ const { unmount } = renderWithProviders(
451
+ <Avatar size={size} fallback={size.toUpperCase()} />
452
+ );
453
+
454
+ const avatar = getAvatarContainer(size.toUpperCase());
455
+ if (size === 'md') {
456
+ expect(avatar).toHaveClass('h-10', 'w-10');
457
+ } else {
458
+ expect(avatar).toHaveClass(sizeClasses[size].split(' ')[0], sizeClasses[size].split(' ')[1]);
459
+ }
460
+
461
+ unmount();
462
+ });
463
+ });
490
464
  });
491
- });
465
+ });