@jmruthers/pace-core 0.5.1 → 0.5.4

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 (210) hide show
  1. package/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
  2. package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
  3. package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
  4. package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
  5. package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
  6. package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
  7. package/dist/chunk-5H3C2SWM.js.map +1 -0
  8. package/dist/chunk-5SIXIV7R.js +1925 -0
  9. package/dist/chunk-5SIXIV7R.js.map +1 -0
  10. package/dist/chunk-GNTALZV3.js +17 -0
  11. package/dist/chunk-GNTALZV3.js.map +1 -0
  12. package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
  13. package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
  14. package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
  15. package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
  16. package/dist/chunk-HXX35Q2M.js.map +1 -0
  17. package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
  18. package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
  19. package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
  20. package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
  21. package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
  22. package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
  23. package/dist/chunk-QVYBYGT2.js +428 -0
  24. package/dist/chunk-QVYBYGT2.js.map +1 -0
  25. package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
  26. package/dist/chunk-WJARTBCT.js.map +1 -0
  27. package/dist/components.d.ts +4 -3
  28. package/dist/components.js +16 -162
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.d.ts +2 -2
  31. package/dist/hooks.js +7 -9
  32. package/dist/hooks.js.map +1 -1
  33. package/dist/index.d.ts +8 -6
  34. package/dist/index.js +152 -17
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers.d.ts +3 -2
  37. package/dist/providers.js +6 -12
  38. package/dist/rbac/index.d.ts +167 -98
  39. package/dist/rbac/index.js +48 -1881
  40. package/dist/rbac/index.js.map +1 -1
  41. package/dist/styles/core.css +0 -55
  42. package/dist/types.d.ts +2 -2
  43. package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
  44. package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
  45. package/dist/utils.js +12 -14
  46. package/dist/utils.js.map +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +73 -0
  49. package/docs/api/classes/MissingUserContextError.md +66 -0
  50. package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
  51. package/docs/api/classes/PermissionDeniedError.md +73 -0
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +270 -0
  54. package/docs/api/classes/RBACCache.md +284 -0
  55. package/docs/api/classes/RBACEngine.md +141 -0
  56. package/docs/api/classes/RBACError.md +76 -0
  57. package/docs/api/classes/RBACNotInitializedError.md +66 -0
  58. package/docs/api/classes/SecureSupabaseClient.md +135 -0
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +96 -0
  65. package/docs/api/interfaces/DataTableAction.md +1 -1
  66. package/docs/api/interfaces/DataTableColumn.md +1 -1
  67. package/docs/api/interfaces/DataTableProps.md +1 -1
  68. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  69. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  70. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +235 -0
  71. package/docs/api/interfaces/EventContextType.md +1 -1
  72. package/docs/api/interfaces/EventLogoProps.md +1 -1
  73. package/docs/api/interfaces/EventProviderProps.md +1 -1
  74. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  75. package/docs/api/interfaces/FileUploadProps.md +1 -1
  76. package/docs/api/interfaces/FooterProps.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoginFormProps.md +1 -1
  81. package/docs/api/interfaces/NavigationAccessRecord.md +107 -0
  82. package/docs/api/interfaces/NavigationContextType.md +164 -0
  83. package/docs/api/interfaces/NavigationGuardProps.md +139 -0
  84. package/docs/api/interfaces/NavigationItem.md +1 -1
  85. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  86. package/docs/api/interfaces/NavigationProviderProps.md +117 -0
  87. package/docs/api/interfaces/Organisation.md +1 -1
  88. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  89. package/docs/api/interfaces/OrganisationMembership.md +2 -2
  90. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  91. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  92. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  93. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  94. package/docs/api/interfaces/PageAccessRecord.md +85 -0
  95. package/docs/api/interfaces/PagePermissionContextType.md +140 -0
  96. package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
  97. package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
  98. package/docs/api/interfaces/PaletteData.md +1 -1
  99. package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
  100. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  102. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  103. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  106. package/docs/api/interfaces/RBACConfig.md +99 -0
  107. package/docs/api/interfaces/RBACContextType.md +474 -0
  108. package/docs/api/interfaces/RBACLogger.md +112 -0
  109. package/docs/api/interfaces/RBACProviderProps.md +107 -0
  110. package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
  111. package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
  112. package/docs/api/interfaces/RouteAccessRecord.md +107 -0
  113. package/docs/api/interfaces/RouteConfig.md +121 -0
  114. package/docs/api/interfaces/SecureDataContextType.md +168 -0
  115. package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
  116. package/docs/api/interfaces/StorageConfig.md +1 -1
  117. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  118. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  119. package/docs/api/interfaces/StorageListOptions.md +1 -1
  120. package/docs/api/interfaces/StorageListResult.md +1 -1
  121. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  122. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  123. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  124. package/docs/api/interfaces/StyleImport.md +1 -1
  125. package/docs/api/interfaces/ToastActionElement.md +1 -1
  126. package/docs/api/interfaces/ToastProps.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
  128. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  130. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  136. package/docs/api/interfaces/UserEventAccess.md +11 -11
  137. package/docs/api/interfaces/UserMenuProps.md +1 -1
  138. package/docs/api/interfaces/UserProfile.md +1 -1
  139. package/docs/api/modules.md +2244 -3
  140. package/docs/migration-guide.md +43 -18
  141. package/docs/styles/README.md +187 -98
  142. package/docs/usage.md +32 -7
  143. package/package.json +2 -2
  144. package/src/components/Footer/Footer.test.tsx +482 -0
  145. package/src/components/Form/Form.test.tsx +1158 -0
  146. package/src/components/Header/Header.test.tsx +582 -0
  147. package/src/components/Header/Header.tsx +1 -1
  148. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
  149. package/src/components/Input/Input.test.tsx +466 -0
  150. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
  151. package/src/components/LoginForm/LoginForm.test.tsx +816 -0
  152. package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
  153. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
  154. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
  155. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
  156. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
  157. package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
  158. package/src/components/Select/Select.test.tsx +948 -0
  159. package/src/components/SuperAdminGuard.tsx +1 -1
  160. package/src/components/Toast/Toast.test.tsx +586 -0
  161. package/src/components/Tooltip/Tooltip.test.tsx +852 -0
  162. package/src/components/UserMenu/UserMenu.test.tsx +702 -0
  163. package/src/components/UserMenu/UserMenu.tsx +2 -2
  164. package/src/hooks/useDebounce.test.ts +375 -0
  165. package/src/hooks/useOrganisationPermissions.test.ts +528 -0
  166. package/src/hooks/useOrganisationSecurity.test.ts +734 -0
  167. package/src/hooks/usePermissionCache.test.ts +542 -0
  168. package/src/hooks/usePermissionCache.ts +1 -1
  169. package/src/index.ts +2 -3
  170. package/src/providers/UnifiedAuthProvider.tsx +2 -2
  171. package/src/providers/index.ts +3 -1
  172. package/src/rbac/__tests__/integration.test.tsx +218 -0
  173. package/src/rbac/api.test.ts +952 -0
  174. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
  175. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
  176. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
  177. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
  178. package/src/rbac/hooks/index.ts +21 -0
  179. package/src/rbac/hooks/useCan.test.ts +461 -0
  180. package/src/rbac/hooks/usePermissions.test.ts +364 -0
  181. package/src/rbac/hooks/usePermissions.ts +567 -0
  182. package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
  183. package/src/rbac/hooks/useRBAC.test.ts +551 -0
  184. package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
  185. package/src/rbac/index.ts +5 -10
  186. package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
  187. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
  188. package/src/rbac/providers/index.ts +11 -0
  189. package/src/styles/core.css +0 -55
  190. package/src/utils/formatDate.test.ts +241 -0
  191. package/dist/chunk-AUE24LVR.js +0 -268
  192. package/dist/chunk-AUE24LVR.js.map +0 -1
  193. package/dist/chunk-COBPIXXQ.js +0 -379
  194. package/dist/chunk-COBPIXXQ.js.map +0 -1
  195. package/dist/chunk-OEGRKULD.js.map +0 -1
  196. package/dist/chunk-OYRY44Q2.js +0 -62
  197. package/dist/chunk-OYRY44Q2.js.map +0 -1
  198. package/dist/chunk-T3XIA4AJ.js.map +0 -1
  199. package/dist/chunk-TGDCLPP2.js.map +0 -1
  200. package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
  201. package/src/components/RBAC/RBACGuard.tsx +0 -143
  202. package/src/components/RBAC/RBACProvider.tsx +0 -186
  203. package/src/components/RBAC/RoleBasedContent.tsx +0 -129
  204. package/src/components/RBAC/index.ts +0 -23
  205. package/src/rbac/hooks.ts +0 -570
  206. /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
  207. /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
  208. /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
  209. /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
  210. /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
@@ -0,0 +1,482 @@
1
+ /**
2
+ * @file Footer Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/Footer
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive test suite for the Footer component.
8
+ * Tests rendering, props handling, navigation links, and accessibility.
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+ import { render, screen } from '@testing-library/react';
13
+ import { Footer, FooterProps } from './Footer';
14
+ import { renderWithProviders } from '../../__tests__/helpers/test-utils';
15
+
16
+ // Mock the current year for consistent testing
17
+ const mockCurrentYear = 2024;
18
+ vi.mock('../../utils/cn', () => ({
19
+ cn: (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')
20
+ }));
21
+
22
+ // Mock Date for consistent year testing
23
+ const mockDate = new Date('2024-01-01');
24
+ vi.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
25
+
26
+ describe('Footer Component', () => {
27
+ beforeEach(() => {
28
+ vi.clearAllMocks();
29
+ });
30
+
31
+ describe('Rendering', () => {
32
+ it('renders with default props', () => {
33
+ renderWithProviders(<Footer />);
34
+
35
+ const footer = screen.getByRole('contentinfo');
36
+ expect(footer).toBeInTheDocument();
37
+ expect(footer).toHaveClass('mt-8', 'py-6', 'flex', 'justify-center', 'border-t', 'border-border', 'bg-main-100');
38
+ });
39
+
40
+ it('renders with custom className', () => {
41
+ renderWithProviders(<Footer className="custom-footer-class" />);
42
+
43
+ const footer = screen.getByRole('contentinfo');
44
+ expect(footer).toHaveClass('custom-footer-class');
45
+ });
46
+
47
+ it('renders with default company name and year', () => {
48
+ renderWithProviders(<Footer />);
49
+
50
+ expect(screen.getByText(/© Copyright 2022–2024 all rights reserved, Solvera Solutions Pty Ltd\./)).toBeInTheDocument();
51
+ });
52
+
53
+ it('renders with custom company name', () => {
54
+ renderWithProviders(<Footer companyName="Test Company Inc." />);
55
+
56
+ expect(screen.getByText(/© Copyright 2022–2024 all rights reserved, Test Company Inc\./)).toBeInTheDocument();
57
+ });
58
+
59
+ it('renders with custom year', () => {
60
+ renderWithProviders(<Footer year={2025} />);
61
+
62
+ expect(screen.getByText(/© Copyright 2022–2025 all rights reserved, Solvera Solutions Pty Ltd\./)).toBeInTheDocument();
63
+ });
64
+
65
+ it('renders with custom copyright text', () => {
66
+ const customCopyright = '© 2024 Custom Company. All rights reserved.';
67
+ renderWithProviders(<Footer copyright={customCopyright} />);
68
+
69
+ expect(screen.getByText(customCopyright)).toBeInTheDocument();
70
+ });
71
+
72
+ it('renders with logo when provided', () => {
73
+ renderWithProviders(<Footer logo="/test-logo.png" />);
74
+
75
+ const logo = screen.getByAltText('Logo');
76
+ expect(logo).toBeInTheDocument();
77
+ expect(logo).toHaveAttribute('src', '/test-logo.png');
78
+ expect(logo).toHaveClass('h-8', 'w-auto');
79
+ });
80
+
81
+ it('renders with children content', () => {
82
+ renderWithProviders(
83
+ <Footer>
84
+ <div data-testid="custom-content">Custom footer content</div>
85
+ </Footer>
86
+ );
87
+
88
+ expect(screen.getByTestId('custom-content')).toBeInTheDocument();
89
+ expect(screen.getByText('Custom footer content')).toBeInTheDocument();
90
+ });
91
+ });
92
+
93
+ describe('Navigation Links', () => {
94
+ it('renders navigation links when provided', () => {
95
+ const links = [
96
+ { label: 'Privacy Policy', href: '/privacy' },
97
+ { label: 'Terms of Service', href: '/terms' },
98
+ { label: 'Contact', href: '/contact' }
99
+ ];
100
+
101
+ renderWithProviders(<Footer links={links} />);
102
+
103
+ expect(screen.getByRole('link', { name: 'Privacy Policy' })).toBeInTheDocument();
104
+ expect(screen.getByRole('link', { name: 'Terms of Service' })).toBeInTheDocument();
105
+ expect(screen.getByRole('link', { name: 'Contact' })).toBeInTheDocument();
106
+ });
107
+
108
+ it('renders links with correct href attributes', () => {
109
+ const links = [
110
+ { label: 'Privacy Policy', href: '/privacy' },
111
+ { label: 'Contact', href: '/contact' }
112
+ ];
113
+
114
+ renderWithProviders(<Footer links={links} />);
115
+
116
+ const privacyLink = screen.getByRole('link', { name: 'Privacy Policy' });
117
+ const contactLink = screen.getByRole('link', { name: 'Contact' });
118
+
119
+ expect(privacyLink).toHaveAttribute('href', '/privacy');
120
+ expect(contactLink).toHaveAttribute('href', '/contact');
121
+ });
122
+
123
+ it('renders links with correct styling classes', () => {
124
+ const links = [{ label: 'Test Link', href: '/test' }];
125
+
126
+ renderWithProviders(<Footer links={links} />);
127
+
128
+ const link = screen.getByRole('link', { name: 'Test Link' });
129
+ expect(link).toHaveClass('text-muted-foreground', 'hover:text-foreground');
130
+ });
131
+
132
+ it('renders links in a list with correct structure', () => {
133
+ const links = [
134
+ { label: 'Link 1', href: '/link1' },
135
+ { label: 'Link 2', href: '/link2' }
136
+ ];
137
+
138
+ renderWithProviders(<Footer links={links} />);
139
+
140
+ const list = screen.getByRole('list');
141
+ expect(list).toBeInTheDocument();
142
+ expect(list).toHaveClass('flex', 'gap-4', 'mt-2', 'md:mt-0');
143
+
144
+ const listItems = screen.getAllByRole('listitem');
145
+ expect(listItems).toHaveLength(2);
146
+ });
147
+
148
+ it('does not render navigation section when no links provided', () => {
149
+ renderWithProviders(<Footer />);
150
+
151
+ expect(screen.queryByRole('list')).not.toBeInTheDocument();
152
+ });
153
+
154
+ it('does not render navigation section when empty links array provided', () => {
155
+ renderWithProviders(<Footer links={[]} />);
156
+
157
+ expect(screen.queryByRole('list')).not.toBeInTheDocument();
158
+ });
159
+ });
160
+
161
+ describe('Content Layout', () => {
162
+ it('renders content in proper semantic structure', () => {
163
+ renderWithProviders(
164
+ <Footer>
165
+ <div>Custom content</div>
166
+ </Footer>
167
+ );
168
+
169
+ const section = document.querySelector('section');
170
+ expect(section).toBeInTheDocument();
171
+ expect(section).toHaveClass('px-4', 'w-[min(var(--app-width),100%)]', 'mx-auto', 'text-center');
172
+ });
173
+
174
+ it('renders logo before children content', () => {
175
+ renderWithProviders(
176
+ <Footer logo="/logo.png">
177
+ <div data-testid="children">Children content</div>
178
+ </Footer>
179
+ );
180
+
181
+ const logo = screen.getByAltText('Logo');
182
+ const children = screen.getByTestId('children');
183
+
184
+ // Logo should appear before children in the DOM
185
+ expect(logo.compareDocumentPosition(children) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
186
+ });
187
+
188
+ it('renders copyright text after children content', () => {
189
+ renderWithProviders(
190
+ <Footer>
191
+ <div data-testid="children">Children content</div>
192
+ </Footer>
193
+ );
194
+
195
+ const children = screen.getByTestId('children');
196
+ const copyright = screen.getByText(/© Copyright/);
197
+
198
+ // Copyright should appear after children in the DOM
199
+ expect(copyright.compareDocumentPosition(children) & Node.DOCUMENT_POSITION_PRECEDING).toBeTruthy();
200
+ });
201
+
202
+ it('renders navigation links after copyright text', () => {
203
+ const links = [{ label: 'Test Link', href: '/test' }];
204
+
205
+ renderWithProviders(<Footer links={links} />);
206
+
207
+ const copyright = screen.getByText(/© Copyright/);
208
+ const link = screen.getByRole('link', { name: 'Test Link' });
209
+
210
+ // Both elements should be present
211
+ expect(copyright).toBeInTheDocument();
212
+ expect(link).toBeInTheDocument();
213
+
214
+ // The link should be in a ul element
215
+ const linkUl = link.closest('ul');
216
+ expect(linkUl).toBeInTheDocument();
217
+ expect(linkUl).toHaveClass('flex', 'gap-4', 'mt-2', 'md:mt-0');
218
+ });
219
+ });
220
+
221
+ describe('Accessibility', () => {
222
+ it('has proper semantic role', () => {
223
+ renderWithProviders(<Footer />);
224
+
225
+ const footer = screen.getByRole('contentinfo');
226
+ expect(footer).toBeInTheDocument();
227
+ });
228
+
229
+ it('has proper heading structure when children contain headings', () => {
230
+ renderWithProviders(
231
+ <Footer>
232
+ <h3>About Us</h3>
233
+ <p>Company description</p>
234
+ </Footer>
235
+ );
236
+
237
+ const heading = screen.getByRole('heading', { level: 3, name: 'About Us' });
238
+ expect(heading).toBeInTheDocument();
239
+ });
240
+
241
+ it('provides accessible navigation links', () => {
242
+ const links = [
243
+ { label: 'Privacy Policy', href: '/privacy' },
244
+ { label: 'Terms of Service', href: '/terms' }
245
+ ];
246
+
247
+ renderWithProviders(<Footer links={links} />);
248
+
249
+ const privacyLink = screen.getByRole('link', { name: 'Privacy Policy' });
250
+ const termsLink = screen.getByRole('link', { name: 'Terms of Service' });
251
+
252
+ expect(privacyLink).toBeInTheDocument();
253
+ expect(termsLink).toBeInTheDocument();
254
+ });
255
+
256
+ it('has proper alt text for logo', () => {
257
+ renderWithProviders(<Footer logo="/logo.png" />);
258
+
259
+ const logo = screen.getByAltText('Logo');
260
+ expect(logo).toBeInTheDocument();
261
+ });
262
+
263
+ it('supports keyboard navigation for links', () => {
264
+ const links = [{ label: 'Test Link', href: '/test' }];
265
+
266
+ renderWithProviders(<Footer links={links} />);
267
+
268
+ const link = screen.getByRole('link', { name: 'Test Link' });
269
+ expect(link).toBeInTheDocument();
270
+ expect(link).toHaveAttribute('href', '/test');
271
+ });
272
+ });
273
+
274
+ describe('Edge Cases', () => {
275
+ it('handles empty string company name', () => {
276
+ renderWithProviders(<Footer companyName="" />);
277
+
278
+ expect(screen.getByText(/© Copyright 2022–2024 all rights reserved, \./)).toBeInTheDocument();
279
+ });
280
+
281
+ it('handles zero year', () => {
282
+ renderWithProviders(<Footer year={0} />);
283
+
284
+ expect(screen.getByText(/© Copyright 2022–0 all rights reserved, Solvera Solutions Pty Ltd\./)).toBeInTheDocument();
285
+ });
286
+
287
+ it('handles negative year', () => {
288
+ renderWithProviders(<Footer year={-1} />);
289
+
290
+ expect(screen.getByText(/© Copyright 2022–-1 all rights reserved, Solvera Solutions Pty Ltd\./)).toBeInTheDocument();
291
+ });
292
+
293
+ it('handles very large year', () => {
294
+ renderWithProviders(<Footer year={9999} />);
295
+
296
+ expect(screen.getByText(/© Copyright 2022–9999 all rights reserved, Solvera Solutions Pty Ltd\./)).toBeInTheDocument();
297
+ });
298
+
299
+ it('handles empty copyright text', () => {
300
+ renderWithProviders(<Footer copyright="" />);
301
+
302
+ // When copyright is empty string, it falls back to default copyright
303
+ expect(screen.getByText(/© Copyright 2022–2024 all rights reserved, Solvera Solutions Pty Ltd\./)).toBeInTheDocument();
304
+ });
305
+
306
+ it('handles special characters in company name', () => {
307
+ renderWithProviders(<Footer companyName="Company & Co. (Ltd.)" />);
308
+
309
+ expect(screen.getByText(/© Copyright 2022–2024 all rights reserved, Company & Co\. \(Ltd\.\)\./)).toBeInTheDocument();
310
+ });
311
+
312
+ it('handles very long company name', () => {
313
+ const longName = 'A'.repeat(100);
314
+ renderWithProviders(<Footer companyName={longName} />);
315
+
316
+ expect(screen.getByText(new RegExp(`© Copyright 2022–2024 all rights reserved, ${longName}\\.`))).toBeInTheDocument();
317
+ });
318
+
319
+ it('handles links with special characters', () => {
320
+ const links = [
321
+ { label: 'Privacy & Terms', href: '/privacy?ref=footer&utm_source=web' },
322
+ { label: 'Contact Us', href: '/contact#support' }
323
+ ];
324
+
325
+ renderWithProviders(<Footer links={links} />);
326
+
327
+ expect(screen.getByRole('link', { name: 'Privacy & Terms' })).toHaveAttribute('href', '/privacy?ref=footer&utm_source=web');
328
+ expect(screen.getByRole('link', { name: 'Contact Us' })).toHaveAttribute('href', '/contact#support');
329
+ });
330
+
331
+ it('handles empty logo URL', () => {
332
+ renderWithProviders(<Footer logo="" />);
333
+
334
+ // Should not render logo when URL is empty
335
+ expect(screen.queryByAltText('Logo')).not.toBeInTheDocument();
336
+ });
337
+
338
+ it('handles undefined logo URL', () => {
339
+ renderWithProviders(<Footer logo={undefined} />);
340
+
341
+ // Should not render logo when URL is undefined
342
+ expect(screen.queryByAltText('Logo')).not.toBeInTheDocument();
343
+ });
344
+ });
345
+
346
+ describe('Props Validation', () => {
347
+ it('uses default values when props are not provided', () => {
348
+ renderWithProviders(<Footer />);
349
+
350
+ expect(screen.getByText(/Solvera Solutions Pty Ltd/)).toBeInTheDocument();
351
+ expect(screen.getByText(/2024/)).toBeInTheDocument();
352
+ });
353
+
354
+ it('overrides default values when props are provided', () => {
355
+ renderWithProviders(
356
+ <Footer
357
+ companyName="Custom Company"
358
+ year={2025}
359
+ className="custom-class"
360
+ />
361
+ );
362
+
363
+ expect(screen.getByText(/Custom Company/)).toBeInTheDocument();
364
+ expect(screen.getByText(/2025/)).toBeInTheDocument();
365
+
366
+ const footer = screen.getByRole('contentinfo');
367
+ expect(footer).toHaveClass('custom-class');
368
+ });
369
+
370
+ it('handles all props being provided', () => {
371
+ const links = [{ label: 'Test', href: '/test' }];
372
+
373
+ renderWithProviders(
374
+ <Footer
375
+ companyName="Test Company"
376
+ year={2023}
377
+ links={links}
378
+ className="test-class"
379
+ logo="/test-logo.png"
380
+ copyright="Custom copyright text"
381
+ >
382
+ <div>Test children</div>
383
+ </Footer>
384
+ );
385
+
386
+ expect(screen.getByText('Custom copyright text')).toBeInTheDocument();
387
+ expect(screen.getByText('Test children')).toBeInTheDocument();
388
+ expect(screen.getByAltText('Logo')).toHaveAttribute('src', '/test-logo.png');
389
+ expect(screen.getByRole('link', { name: 'Test' })).toBeInTheDocument();
390
+
391
+ const footer = screen.getByRole('contentinfo');
392
+ expect(footer).toHaveClass('test-class');
393
+ });
394
+ });
395
+
396
+ describe('Integration', () => {
397
+ it('works with complex children content', () => {
398
+ renderWithProviders(
399
+ <Footer>
400
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
401
+ <div>
402
+ <h3 className="font-semibold mb-2">About Us</h3>
403
+ <p className="text-sm text-muted-foreground">
404
+ We provide innovative solutions for modern businesses.
405
+ </p>
406
+ </div>
407
+ <div>
408
+ <h3 className="font-semibold mb-2">Contact</h3>
409
+ <p className="text-sm text-muted-foreground">
410
+ Email: info@company.com
411
+ </p>
412
+ </div>
413
+ <div>
414
+ <h3 className="font-semibold mb-2">Follow Us</h3>
415
+ <div className="flex gap-2">
416
+ <a href="#" className="text-muted-foreground hover:text-foreground">Twitter</a>
417
+ <a href="#" className="text-muted-foreground hover:text-foreground">LinkedIn</a>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ </Footer>
422
+ );
423
+
424
+ expect(screen.getByRole('heading', { name: 'About Us' })).toBeInTheDocument();
425
+ expect(screen.getByRole('heading', { name: 'Contact' })).toBeInTheDocument();
426
+ expect(screen.getByRole('heading', { name: 'Follow Us' })).toBeInTheDocument();
427
+ expect(screen.getByText('We provide innovative solutions for modern businesses.')).toBeInTheDocument();
428
+ expect(screen.getByText('Email: info@company.com')).toBeInTheDocument();
429
+ expect(screen.getByRole('link', { name: 'Twitter' })).toBeInTheDocument();
430
+ expect(screen.getByRole('link', { name: 'LinkedIn' })).toBeInTheDocument();
431
+ });
432
+
433
+ it('works with multiple navigation links', () => {
434
+ const links = [
435
+ { label: 'Home', href: '/' },
436
+ { label: 'About', href: '/about' },
437
+ { label: 'Services', href: '/services' },
438
+ { label: 'Portfolio', href: '/portfolio' },
439
+ { label: 'Blog', href: '/blog' },
440
+ { label: 'Contact', href: '/contact' }
441
+ ];
442
+
443
+ renderWithProviders(<Footer links={links} />);
444
+
445
+ links.forEach(link => {
446
+ expect(screen.getByRole('link', { name: link.label })).toBeInTheDocument();
447
+ expect(screen.getByRole('link', { name: link.label })).toHaveAttribute('href', link.href);
448
+ });
449
+ });
450
+
451
+ it('maintains proper structure with all features combined', () => {
452
+ const links = [
453
+ { label: 'Privacy', href: '/privacy' },
454
+ { label: 'Terms', href: '/terms' }
455
+ ];
456
+
457
+ renderWithProviders(
458
+ <Footer
459
+ companyName="Test Company"
460
+ year={2024}
461
+ links={links}
462
+ logo="/logo.png"
463
+ className="custom-footer"
464
+ >
465
+ <div data-testid="custom-content">Custom footer content</div>
466
+ </Footer>
467
+ );
468
+
469
+ // Check all elements are present
470
+ expect(screen.getByRole('contentinfo')).toBeInTheDocument();
471
+ expect(screen.getByAltText('Logo')).toBeInTheDocument();
472
+ expect(screen.getByTestId('custom-content')).toBeInTheDocument();
473
+ expect(screen.getByText(/Test Company/)).toBeInTheDocument();
474
+ expect(screen.getByRole('link', { name: 'Privacy' })).toBeInTheDocument();
475
+ expect(screen.getByRole('link', { name: 'Terms' })).toBeInTheDocument();
476
+
477
+ // Check styling
478
+ const footer = screen.getByRole('contentinfo');
479
+ expect(footer).toHaveClass('custom-footer');
480
+ });
481
+ });
482
+ });