@jmruthers/pace-core 0.5.89 → 0.5.91

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 (160) hide show
  1. package/dist/{AuthService-DcTI5Ov4.d.ts → AuthService-DX15wM6y.d.ts} +8 -0
  2. package/dist/{DataTable-PWBMKMOG.js → DataTable-VIP44OB6.js} +7 -6
  3. package/dist/{PublicLoadingSpinner-BQXD1fbO.d.ts → PublicLoadingSpinner-Dx5c2g3S.d.ts} +9 -3
  4. package/dist/{UnifiedAuthProvider-5D3HEQND.js → UnifiedAuthProvider-6JRTOFPS.js} +4 -3
  5. package/dist/{chunk-BNXBJOGL.js → chunk-4DYK5KCK.js} +4 -4
  6. package/dist/{chunk-KWICIQVK.js → chunk-7NIERLC6.js} +8 -8
  7. package/dist/{chunk-KWICIQVK.js.map → chunk-7NIERLC6.js.map} +1 -1
  8. package/dist/{chunk-KTPG5VCH.js → chunk-7XBW2P7B.js} +2 -2
  9. package/dist/{chunk-DP5X5ORK.js → chunk-AIV3VYBQ.js} +82 -25
  10. package/dist/chunk-AIV3VYBQ.js.map +1 -0
  11. package/dist/{chunk-YY4YYM3E.js → chunk-G2SCPUKC.js} +2 -2
  12. package/dist/{chunk-CXKMRKRF.js → chunk-G2YT64FA.js} +3 -3
  13. package/dist/{chunk-AQGF5OG7.js → chunk-GD3ENUKD.js} +3 -3
  14. package/dist/{chunk-7VJDS5QD.js → chunk-JDPFQV3V.js} +4 -4
  15. package/dist/{chunk-3RZBKQ5Y.js → chunk-JQWSAYZC.js} +2 -2
  16. package/dist/{chunk-A6HBIY5P.js → chunk-SMJZMKYN.js} +11 -2
  17. package/dist/{chunk-A6HBIY5P.js.map → chunk-SMJZMKYN.js.map} +1 -1
  18. package/dist/{chunk-YWAFPVJA.js → chunk-VJJNZKHO.js} +40 -18
  19. package/dist/chunk-VJJNZKHO.js.map +1 -0
  20. package/dist/{chunk-L3RV2ALE.js → chunk-VKOCWWVY.js} +6 -1
  21. package/dist/{chunk-L3RV2ALE.js.map → chunk-VKOCWWVY.js.map} +1 -1
  22. package/dist/{chunk-6UHXQH7P.js → chunk-XZHZYSAK.js} +5 -5
  23. package/dist/components.d.ts +1 -1
  24. package/dist/components.js +9 -8
  25. package/dist/components.js.map +1 -1
  26. package/dist/hooks.js +8 -8
  27. package/dist/index.d.ts +3 -3
  28. package/dist/index.js +13 -13
  29. package/dist/providers.d.ts +2 -2
  30. package/dist/providers.js +3 -2
  31. package/dist/rbac/index.js +8 -7
  32. package/dist/styles/index.js +2 -2
  33. package/dist/theming/runtime.js +3 -1
  34. package/dist/utils.js +1 -1
  35. package/docs/api/classes/ColumnFactory.md +1 -1
  36. package/docs/api/classes/ErrorBoundary.md +1 -1
  37. package/docs/api/classes/InvalidScopeError.md +1 -1
  38. package/docs/api/classes/MissingUserContextError.md +1 -1
  39. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  40. package/docs/api/classes/PermissionDeniedError.md +1 -1
  41. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  42. package/docs/api/classes/RBACAuditManager.md +1 -1
  43. package/docs/api/classes/RBACCache.md +1 -1
  44. package/docs/api/classes/RBACEngine.md +1 -1
  45. package/docs/api/classes/RBACError.md +1 -1
  46. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  47. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  48. package/docs/api/classes/StorageUtils.md +1 -1
  49. package/docs/api/enums/FileCategory.md +1 -1
  50. package/docs/api/interfaces/AggregateConfig.md +1 -1
  51. package/docs/api/interfaces/ButtonProps.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/DataAccessRecord.md +1 -1
  56. package/docs/api/interfaces/DataRecord.md +1 -1
  57. package/docs/api/interfaces/DataTableAction.md +1 -1
  58. package/docs/api/interfaces/DataTableColumn.md +1 -1
  59. package/docs/api/interfaces/DataTableProps.md +1 -1
  60. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  61. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  62. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  63. package/docs/api/interfaces/EventLogoProps.md +1 -1
  64. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  65. package/docs/api/interfaces/FileMetadata.md +1 -1
  66. package/docs/api/interfaces/FileReference.md +1 -1
  67. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  68. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  69. package/docs/api/interfaces/FileUploadProps.md +1 -1
  70. package/docs/api/interfaces/FooterProps.md +1 -1
  71. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  72. package/docs/api/interfaces/InputProps.md +1 -1
  73. package/docs/api/interfaces/LabelProps.md +1 -1
  74. package/docs/api/interfaces/LoginFormProps.md +1 -1
  75. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  76. package/docs/api/interfaces/NavigationContextType.md +1 -1
  77. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  78. package/docs/api/interfaces/NavigationItem.md +1 -1
  79. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  80. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  81. package/docs/api/interfaces/Organisation.md +1 -1
  82. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  83. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  84. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  85. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  86. package/docs/api/interfaces/PaceAppLayoutProps.md +34 -21
  87. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  88. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  89. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  90. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  91. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  92. package/docs/api/interfaces/PaletteData.md +1 -1
  93. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  94. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  95. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  96. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  97. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  98. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  99. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  100. package/docs/api/interfaces/RBACConfig.md +1 -1
  101. package/docs/api/interfaces/RBACLogger.md +1 -1
  102. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  103. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  104. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  105. package/docs/api/interfaces/RouteConfig.md +1 -1
  106. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  107. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  108. package/docs/api/interfaces/StorageConfig.md +1 -1
  109. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  110. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  111. package/docs/api/interfaces/StorageListOptions.md +1 -1
  112. package/docs/api/interfaces/StorageListResult.md +1 -1
  113. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  114. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  115. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  116. package/docs/api/interfaces/StyleImport.md +1 -1
  117. package/docs/api/interfaces/SwitchProps.md +1 -1
  118. package/docs/api/interfaces/ToastActionElement.md +1 -1
  119. package/docs/api/interfaces/ToastProps.md +1 -1
  120. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  121. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  122. package/docs/api/interfaces/UseEventLogoOptions.md +1 -1
  123. package/docs/api/interfaces/UseEventLogoReturn.md +1 -1
  124. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  125. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  126. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  127. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  128. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  129. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  130. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  131. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  132. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  133. package/docs/api/interfaces/UserEventAccess.md +1 -1
  134. package/docs/api/interfaces/UserMenuProps.md +1 -1
  135. package/docs/api/interfaces/UserProfile.md +1 -1
  136. package/docs/api/modules.md +9 -7
  137. package/package.json +1 -1
  138. package/src/components/Header/Header.tsx +52 -15
  139. package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
  140. package/src/components/PaceAppLayout/README.md +30 -0
  141. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +10 -0
  142. package/src/components/PaceLoginPage/PaceLoginPage.tsx +20 -14
  143. package/src/hooks/public/usePublicEvent.ts +0 -2
  144. package/src/services/AuthService.ts +29 -29
  145. package/src/services/EventService.ts +65 -0
  146. package/src/services/__tests__/AuthService.restoreSession.test.ts +35 -0
  147. package/src/services/__tests__/AuthService.test.ts +6 -7
  148. package/src/services/__tests__/EventService.eventColours.test.ts +72 -0
  149. package/dist/chunk-DP5X5ORK.js.map +0 -1
  150. package/dist/chunk-YWAFPVJA.js.map +0 -1
  151. /package/dist/{DataTable-PWBMKMOG.js.map → DataTable-VIP44OB6.js.map} +0 -0
  152. /package/dist/{UnifiedAuthProvider-5D3HEQND.js.map → UnifiedAuthProvider-6JRTOFPS.js.map} +0 -0
  153. /package/dist/{chunk-BNXBJOGL.js.map → chunk-4DYK5KCK.js.map} +0 -0
  154. /package/dist/{chunk-KTPG5VCH.js.map → chunk-7XBW2P7B.js.map} +0 -0
  155. /package/dist/{chunk-YY4YYM3E.js.map → chunk-G2SCPUKC.js.map} +0 -0
  156. /package/dist/{chunk-CXKMRKRF.js.map → chunk-G2YT64FA.js.map} +0 -0
  157. /package/dist/{chunk-AQGF5OG7.js.map → chunk-GD3ENUKD.js.map} +0 -0
  158. /package/dist/{chunk-7VJDS5QD.js.map → chunk-JDPFQV3V.js.map} +0 -0
  159. /package/dist/{chunk-3RZBKQ5Y.js.map → chunk-JQWSAYZC.js.map} +0 -0
  160. /package/dist/{chunk-6UHXQH7P.js.map → chunk-XZHZYSAK.js.map} +0 -0
@@ -292,15 +292,18 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
292
292
  try {
293
293
  const { error } = await signIn(data.email, data.password);
294
294
 
295
- if (!error) {
296
- // Navigation will be handled by the useEffect that checks app access
297
- // Don't navigate here if requireAppAccess is true
298
- if (!requireAppAccess) {
299
- try {
300
- navigate(onSuccessRedirectPath, { replace: true });
301
- } catch (navError) {
302
- console.error('Navigation error after sign-in:', navError);
303
- }
295
+ if (error) {
296
+ // Throw error so LoginForm can catch and display it
297
+ throw error;
298
+ }
299
+
300
+ // Navigation will be handled by the useEffect that checks app access
301
+ // Don't navigate here if requireAppAccess is true
302
+ if (!requireAppAccess) {
303
+ try {
304
+ navigate(onSuccessRedirectPath, { replace: true });
305
+ } catch (navError) {
306
+ console.error('Navigation error after sign-in:', navError);
304
307
  }
305
308
  }
306
309
  } finally {
@@ -325,11 +328,14 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
325
328
  console.error('Login error:', error);
326
329
  }}
327
330
  />
328
- {authError && (
329
- <em className="mt-4 text-destructive text-center">
330
- {authError.message}
331
- </em>
332
- )}
331
+ {(() => {
332
+ const benign = !!(authError && (
333
+ authError.name === 'AuthSessionMissingError' || /Auth session missing/i.test(authError.message)
334
+ ));
335
+ return authError && !benign ? (
336
+ <em className="mt-4 text-destructive text-center">{authError.message}</em>
337
+ ) : null;
338
+ })()}
333
339
  {accessError && (
334
340
  <em className="mt-4 text-destructive text-center">
335
341
  {accessError}
@@ -224,7 +224,6 @@ export function usePublicEvent(
224
224
  event_catering_email,
225
225
  event_news,
226
226
  event_billing,
227
- event_footer,
228
227
  event_email
229
228
  `)
230
229
  .eq('event_code', eventCode)
@@ -301,7 +300,6 @@ export function usePublicEvent(
301
300
  event_catering_email,
302
301
  event_news,
303
302
  event_billing,
304
- event_footer,
305
303
  event_email
306
304
  `)
307
305
  .eq('event_code', eventCode)
@@ -453,42 +453,42 @@ export class AuthService extends BaseService implements IAuthService {
453
453
  // Record error but continue to attempt getUser to satisfy edge cases
454
454
  console.debug('[AuthService] getSession returned error, attempting to fetch user anyway');
455
455
  this.authError = sessionError;
456
+
457
+ // Attempt getUser as fallback when getSession fails
458
+ try {
459
+ const getUserFn = (this.supabaseClient.auth as any)?.getUser as (() => Promise<{ data?: { user?: User | null }, error?: AuthError | null }>) | undefined;
460
+ if (typeof getUserFn === 'function') {
461
+ const userResult = await getUserFn();
462
+ const currentUser = userResult?.data?.user ?? null;
463
+ const userError = userResult?.error ?? null;
464
+
465
+ if (currentUser) {
466
+ this.user = currentUser;
467
+ // If we got a user but no session, we still don't have a valid session
468
+ this.session = null;
469
+ }
470
+ if (userError && !this.authError) {
471
+ this.authError = userError;
472
+ }
473
+ }
474
+ } catch (getUserError) {
475
+ // If getUser also fails, we've already recorded the sessionError
476
+ console.debug('[AuthService] getUser also failed:', getUserError);
477
+ }
456
478
  }
457
479
 
458
480
  if (currentSession) {
459
481
  this.session = currentSession;
460
482
  this.user = currentSession.user;
461
483
  this.authError = null;
462
- } else {
463
- console.debug('[AuthService] No active session found, checking for existing user');
484
+ } else if (!sessionError) {
485
+ // Only skip getUser if we didn't already attempt it due to a sessionError
486
+ // Treat missing session as a normal cold-start state on public pages (e.g., login)
487
+ // Do not call getUser() which can raise AuthSessionMissingError and surface a noisy banner
488
+ console.debug('[AuthService] No active session found; treating as normal unauthenticated state');
464
489
  this.session = null;
465
- // Safely call getUser without destructuring
466
- let currentUser: User | null = null;
467
- let userError: AuthError | null = null;
468
- const getUserFn = (this.supabaseClient.auth as any)?.getUser as (() => Promise<{ data?: { user?: User | null }, error?: AuthError | null }>) | undefined;
469
- if (typeof getUserFn === 'function') {
470
- const userResult = await getUserFn();
471
- currentUser = userResult?.data?.user ?? null;
472
- userError = userResult?.error ?? null;
473
- }
474
-
475
- if (userError) {
476
- console.debug('[AuthService] getUser returned error during restoration');
477
- this.authError = userError;
478
- }
479
-
480
- if (currentUser) {
481
- this.user = currentUser;
482
- console.debug('[AuthService] Found user without active session during restoration');
483
- } else {
484
- this.user = null;
485
- this.session = null;
486
- }
487
-
488
- // Only clear authError if we successfully got user or had no errors
489
- if (!userError && !sessionError) {
490
- this.authError = null;
491
- }
490
+ this.user = null;
491
+ this.authError = null;
492
492
  }
493
493
 
494
494
  // Finish successfully even if earlier calls reported an error, to avoid noisy warnings in benign cases
@@ -14,6 +14,7 @@ import { IEventService } from './interfaces/IEventService';
14
14
  import { Event } from '../types/unified';
15
15
  import { Organisation } from '../types/organisation';
16
16
  import { DebugLogger } from '../utils/debugLogger';
17
+ import { applyPalette, clearPalette } from '../theming/runtime';
17
18
 
18
19
  export class EventService extends BaseService implements IEventService {
19
20
  private events: Event[] = [];
@@ -135,6 +136,8 @@ export class EventService extends BaseService implements IEventService {
135
136
  this.persistEventSelection(event.event_id);
136
137
  // Reset the user cleared flag when selecting an event
137
138
  this.userClearedEventRef = false;
139
+ // Apply theme for the newly selected event
140
+ this.updateThemeForSelectedEvent();
138
141
  } else {
139
142
  this.selectedEvent = null;
140
143
  this.setSelectedEventId?.(null);
@@ -145,6 +148,8 @@ export class EventService extends BaseService implements IEventService {
145
148
  this.hasAutoSelectedRef = false;
146
149
  // Mark that user explicitly cleared the event to prevent auto-selection
147
150
  this.userClearedEventRef = true;
151
+ // Clear theme when event is cleared
152
+ this.updateThemeForSelectedEvent();
148
153
  }
149
154
  this.notify();
150
155
  }
@@ -175,6 +180,8 @@ export class EventService extends BaseService implements IEventService {
175
180
  if (persistedEvent) {
176
181
  this.selectedEvent = persistedEvent;
177
182
  this.setSelectedEventId?.(persistedEventId);
183
+ // Apply theme for persisted event
184
+ this.updateThemeForSelectedEvent();
178
185
  return true;
179
186
  } else {
180
187
  // Clear invalid persisted event
@@ -218,6 +225,8 @@ export class EventService extends BaseService implements IEventService {
218
225
  this.selectedEvent = nextEvent;
219
226
  this.setSelectedEventId?.(nextEvent.event_id);
220
227
  this.persistEventSelection(nextEvent.event_id);
228
+ // Apply theme for auto-selected event
229
+ this.updateThemeForSelectedEvent();
221
230
  }
222
231
  }
223
232
 
@@ -338,6 +347,62 @@ export class EventService extends BaseService implements IEventService {
338
347
  }
339
348
  }
340
349
 
350
+ /**
351
+ * Parse and normalize event_colours to PaletteData (supports ev-* keys and string JSON)
352
+ */
353
+ private parseAndNormalizeEventColours(input: unknown): { main: any; sec: any; acc: any } | null {
354
+ try {
355
+ if (!input) return null;
356
+ let obj: any = input;
357
+ if (typeof input === 'string') {
358
+ try {
359
+ obj = JSON.parse(input);
360
+ } catch {
361
+ return null;
362
+ }
363
+ } else if (typeof input !== 'object') {
364
+ return null;
365
+ }
366
+
367
+ const pick = (o: any, pref: string, plain: string) => (o?.[pref] ?? o?.[plain]) || null;
368
+ const main = pick(obj, 'ev-main', 'main');
369
+ const sec = pick(obj, 'ev-sec', 'sec');
370
+ const acc = pick(obj, 'ev-acc', 'acc');
371
+ if (!main && !sec && !acc) return null;
372
+
373
+ // Fill helper: ensure all TW shades exist using raw or 500 as fallback
374
+ const shades = ['50','100','200','300','400','500','600','700','800','900','950'];
375
+ const fill = (p: any) => {
376
+ if (!p) return {} as any;
377
+ const out: any = {};
378
+ for (const s of shades) out[s] = p[s] || p?.raw || p?.['500'];
379
+ if (p?.raw) out.raw = p.raw;
380
+ return out;
381
+ };
382
+
383
+ return { main: fill(main), sec: fill(sec), acc: fill(acc) };
384
+ } catch (error) {
385
+ console.warn('[EventService] Failed to parse/normalize event colours:', error);
386
+ return null;
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Apply or clear runtime theme variables based on the current selected event's colours
392
+ */
393
+ private updateThemeForSelectedEvent(): void {
394
+ try {
395
+ const normalized = this.parseAndNormalizeEventColours(this.selectedEvent?.event_colours);
396
+ if (normalized) {
397
+ applyPalette(normalized);
398
+ } else {
399
+ clearPalette();
400
+ }
401
+ } catch (error) {
402
+ console.warn('[EventService] Failed to update theme from event colours:', error);
403
+ }
404
+ }
405
+
341
406
  getNextEventByDate(events?: Event[]): Event | null {
342
407
  const eventsToUse = events || this.events;
343
408
  if (!eventsToUse || eventsToUse.length === 0) {
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { AuthService } from '../../services/AuthService';
3
+
4
+ // Minimal Supabase auth mock sufficient for restoreSession/initialize paths
5
+ function createSupabaseMock() {
6
+ const auth = {
7
+ getSession: vi.fn().mockResolvedValue({ data: { session: null }, error: null }),
8
+ getUser: vi.fn(), // should not be called when session is null
9
+ onAuthStateChange: vi.fn().mockImplementation((cb: any) => {
10
+ // Provide a subscription-like object
11
+ return { unsubscribe: () => {} } as any;
12
+ }),
13
+ } as any;
14
+
15
+ return { auth } as any;
16
+ }
17
+
18
+ describe('AuthService.restoreSession (no session startup)', () => {
19
+ it('does not call getUser() and does not set authError when session is null', async () => {
20
+ const supabase = createSupabaseMock();
21
+ const service = new AuthService(supabase);
22
+
23
+ // initialize() triggers setup listener and restoreSession()
24
+ await service.initialize();
25
+
26
+ // Assertions
27
+ expect(supabase.auth.getSession).toHaveBeenCalledTimes(1);
28
+ expect(supabase.auth.getUser).not.toHaveBeenCalled();
29
+ expect(service.getError()).toBeNull();
30
+ expect(service.getUser()).toBeNull();
31
+ expect(service.getSession()).toBeNull();
32
+ });
33
+ });
34
+
35
+
@@ -518,23 +518,22 @@ describe('AuthService', () => {
518
518
  expect(mockSupabase.auth.getUser).toHaveBeenCalled();
519
519
  });
520
520
 
521
- it('should handle initialization with getUser error', async () => {
522
- const userError = new AuthError('User error');
521
+ it('should not call getUser when getSession succeeds with no session (prevents AuthSessionMissingError)', async () => {
523
522
  mockSupabase.auth.getSession.mockResolvedValue({
524
523
  data: { session: null },
525
524
  error: null
526
525
  });
527
- mockSupabase.auth.getUser.mockResolvedValue({
528
- data: { user: null },
529
- error: userError
530
- });
531
526
  mockSupabase.auth.onAuthStateChange.mockReturnValue({
532
527
  data: { subscription: { unsubscribe: vi.fn() } }
533
528
  });
534
529
 
535
530
  await authService.initialize();
536
531
 
537
- expect(mockSupabase.auth.getUser).toHaveBeenCalled();
532
+ // When getSession succeeds with no session, getUser is NOT called to prevent
533
+ // AuthSessionMissingError from being raised on public pages (e.g., login page)
534
+ // This is intentional behavior to avoid noisy error banners for benign unauthenticated states
535
+ expect(mockSupabase.auth.getSession).toHaveBeenCalled();
536
+ expect(mockSupabase.auth.getUser).not.toHaveBeenCalled();
538
537
  });
539
538
 
540
539
  it('should restore session from storage during initialization', async () => {
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { EventService } from '../../services/EventService';
3
+
4
+ function createSupabaseMock() {
5
+ const auth = {
6
+ onAuthStateChange: vi.fn().mockReturnValue({ unsubscribe: () => {} }),
7
+ } as any;
8
+ return { auth } as any;
9
+ }
10
+
11
+ const baseEvent = {
12
+ event_id: 'e1',
13
+ event_name: 'Test',
14
+ organisation_id: 'org1',
15
+ };
16
+
17
+ describe('EventService event_colours normalization', () => {
18
+ it('applies palettes when event_colours uses ev-* keys (object)', async () => {
19
+ const supabase = createSupabaseMock();
20
+ const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
21
+ // @ts-ignore access private for test by casting
22
+ (svc as any).selectedEvent = {
23
+ ...baseEvent,
24
+ event_colours: {
25
+ 'ev-main': { raw: { L: 0.75, C: 0.1, H: 200 }, '500': { L: 0.75, C: 0.1, H: 200 } },
26
+ 'ev-sec': { raw: { L: 0.7, C: 0.12, H: 260 }, '500': { L: 0.7, C: 0.12, H: 260 } },
27
+ 'ev-acc': { raw: { L: 0.65, C: 0.14, H: 20 }, '500': { L: 0.65, C: 0.14, H: 20 } },
28
+ },
29
+ };
30
+
31
+ // Spy on document style updates by calling the method and ensuring no throw
32
+ expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
33
+ });
34
+
35
+ it('parses string JSON with ev-* keys', () => {
36
+ const supabase = createSupabaseMock();
37
+ const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
38
+ const payload = JSON.stringify({
39
+ 'ev-main': { '500': { L: 0.75, C: 0.1, H: 200 }, raw: { L: 0.75, C: 0.1, H: 200 } },
40
+ 'ev-sec': { '500': { L: 0.7, C: 0.12, H: 260 }, raw: { L: 0.7, C: 0.12, H: 260 } },
41
+ 'ev-acc': { '500': { L: 0.65, C: 0.14, H: 20 }, raw: { L: 0.65, C: 0.14, H: 20 } },
42
+ });
43
+ // @ts-ignore private
44
+ (svc as any).selectedEvent = { ...baseEvent, event_colours: payload };
45
+ expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
46
+ });
47
+
48
+ it('accepts plain main/sec/acc', () => {
49
+ const supabase = createSupabaseMock();
50
+ const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
51
+ // @ts-ignore private
52
+ (svc as any).selectedEvent = {
53
+ ...baseEvent,
54
+ event_colours: {
55
+ main: { '500': { L: 0.75, C: 0.1, H: 200 } },
56
+ sec: { '500': { L: 0.7, C: 0.12, H: 260 } },
57
+ acc: { '500': { L: 0.65, C: 0.14, H: 20 } },
58
+ },
59
+ };
60
+ expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
61
+ });
62
+
63
+ it('clears when invalid structure', () => {
64
+ const supabase = createSupabaseMock();
65
+ const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
66
+ // @ts-ignore private
67
+ (svc as any).selectedEvent = { ...baseEvent, event_colours: { something: {} } };
68
+ expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
69
+ });
70
+ });
71
+
72
+