@react-spa-scaffold/mcp 2.2.0 → 2.3.0

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 (112) hide show
  1. package/dist/constants.d.ts +3 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +3 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/features/definitions/database.d.ts +3 -0
  6. package/dist/features/definitions/database.d.ts.map +1 -0
  7. package/dist/features/definitions/database.js +45 -0
  8. package/dist/features/definitions/database.js.map +1 -0
  9. package/dist/features/definitions/deployment.d.ts +3 -0
  10. package/dist/features/definitions/deployment.d.ts.map +1 -0
  11. package/dist/features/definitions/deployment.js +14 -0
  12. package/dist/features/definitions/deployment.js.map +1 -0
  13. package/dist/features/definitions/index.d.ts +2 -0
  14. package/dist/features/definitions/index.d.ts.map +1 -1
  15. package/dist/features/definitions/index.js +2 -0
  16. package/dist/features/definitions/index.js.map +1 -1
  17. package/dist/features/registry.d.ts.map +1 -1
  18. package/dist/features/registry.js +3 -1
  19. package/dist/features/registry.js.map +1 -1
  20. package/dist/features/types.test.js +4 -2
  21. package/dist/features/types.test.js.map +1 -1
  22. package/dist/resources/docs.d.ts.map +1 -1
  23. package/dist/resources/docs.js +5 -0
  24. package/dist/resources/docs.js.map +1 -1
  25. package/dist/tools/add-features.js +1 -1
  26. package/dist/tools/add-features.js.map +1 -1
  27. package/dist/utils/docs.d.ts.map +1 -1
  28. package/dist/utils/docs.js +2 -0
  29. package/dist/utils/docs.js.map +1 -1
  30. package/dist/utils/scaffold/claude-md/index.d.ts.map +1 -1
  31. package/dist/utils/scaffold/claude-md/index.js +3 -1
  32. package/dist/utils/scaffold/claude-md/index.js.map +1 -1
  33. package/dist/utils/scaffold/claude-md/sections.d.ts +2 -0
  34. package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
  35. package/dist/utils/scaffold/claude-md/sections.js +132 -2
  36. package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
  37. package/dist/utils/scaffold/compute.js +1 -1
  38. package/dist/utils/scaffold/compute.js.map +1 -1
  39. package/dist/utils/scaffold/generators.d.ts +2 -2
  40. package/dist/utils/scaffold/generators.d.ts.map +1 -1
  41. package/dist/utils/scaffold/generators.js +57 -22
  42. package/dist/utils/scaffold/generators.js.map +1 -1
  43. package/package.json +1 -1
  44. package/templates/.env.example +40 -12
  45. package/templates/.github/workflows/ci.yml +4 -1
  46. package/templates/.github/workflows/deploy.yml +59 -0
  47. package/templates/CLAUDE.md +177 -1
  48. package/templates/docs/AUTHENTICATION.md +325 -0
  49. package/templates/docs/DEPLOYMENT.md +268 -0
  50. package/templates/docs/E2E_TESTING.md +81 -4
  51. package/templates/docs/SUPABASE_INTEGRATION.md +310 -0
  52. package/templates/docs/TESTING.md +195 -77
  53. package/templates/e2e/auth/auth.setup.ts +60 -0
  54. package/templates/e2e/fixtures/index.ts +11 -0
  55. package/templates/e2e/tests/profile.auth.spec.ts +103 -0
  56. package/templates/e2e/tests/profile.spec.ts +64 -0
  57. package/templates/e2e/tests/register-form.spec.ts +38 -0
  58. package/templates/gitignore +5 -0
  59. package/templates/package.json +8 -0
  60. package/templates/playwright.config.ts +33 -3
  61. package/templates/src/App.tsx +32 -19
  62. package/templates/src/components/layout/Header.test.tsx +17 -1
  63. package/templates/src/components/layout/Header.tsx +11 -0
  64. package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +3 -3
  65. package/templates/src/components/shared/ProfileSync/ProfileSync.test.tsx +44 -0
  66. package/templates/src/components/shared/ProfileSync/ProfileSync.tsx +104 -0
  67. package/templates/src/components/shared/ProfileSync/index.ts +1 -0
  68. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.test.tsx +3 -3
  69. package/templates/src/components/shared/index.ts +1 -0
  70. package/templates/src/contexts/performanceContext.tsx +3 -3
  71. package/templates/src/contexts/supabaseContext.test.tsx +59 -0
  72. package/templates/src/contexts/supabaseContext.tsx +87 -0
  73. package/templates/src/hooks/index.ts +17 -0
  74. package/templates/src/hooks/supabase/index.ts +12 -0
  75. package/templates/src/hooks/supabase/useProfiles.test.tsx +207 -0
  76. package/templates/src/hooks/supabase/useProfiles.ts +213 -0
  77. package/templates/src/hooks/supabase/useSupabaseQuery.test.tsx +150 -0
  78. package/templates/src/hooks/supabase/useSupabaseQuery.ts +91 -0
  79. package/templates/src/lib/api.test.ts +30 -38
  80. package/templates/src/lib/api.ts +1 -7
  81. package/templates/src/lib/config.ts +54 -4
  82. package/templates/src/lib/env.ts +36 -14
  83. package/templates/src/lib/index.ts +4 -2
  84. package/templates/src/lib/routes.ts +1 -0
  85. package/templates/src/lib/sentry.ts +13 -10
  86. package/templates/src/lib/supabase/client.ts +58 -0
  87. package/templates/src/lib/supabase/index.ts +5 -0
  88. package/templates/src/main.tsx +17 -39
  89. package/templates/src/mocks/constants.ts +31 -0
  90. package/templates/src/mocks/fixtures/index.ts +3 -1
  91. package/templates/src/mocks/fixtures/profiles.ts +55 -0
  92. package/templates/src/mocks/fixtures/users.ts +91 -0
  93. package/templates/src/mocks/handlers/index.ts +2 -1
  94. package/templates/src/mocks/handlers/supabase.ts +64 -0
  95. package/templates/src/mocks/handlers/todos.ts +1 -1
  96. package/templates/src/mocks/index.ts +6 -0
  97. package/templates/src/pages/Profile.test.tsx +263 -0
  98. package/templates/src/pages/Profile.tsx +171 -0
  99. package/templates/src/pages/index.ts +1 -0
  100. package/templates/src/stores/preferencesStore.ts +2 -1
  101. package/templates/src/test/clerkMock.tsx +49 -9
  102. package/templates/src/test/fetchMock.ts +58 -0
  103. package/templates/src/test/index.ts +49 -3
  104. package/templates/src/test/mocks.ts +128 -1
  105. package/templates/src/test/providers.tsx +7 -4
  106. package/templates/src/test/supabaseMock.ts +112 -0
  107. package/templates/src/test-setup.ts +26 -0
  108. package/templates/src/types/database.ts +46 -0
  109. package/templates/src/types/index.ts +1 -0
  110. package/templates/src/types/supabase.ts +167 -0
  111. package/templates/src/vite-env.d.ts +6 -0
  112. package/templates/supabase/migrations/20260104000000_create_profiles_table.sql +67 -0
@@ -0,0 +1,171 @@
1
+ import { Trans, useLingui } from '@lingui/react/macro';
2
+ import { useState } from 'react';
3
+
4
+ import { SEO } from '@/components/shared';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
7
+ import { Input } from '@/components/ui/input';
8
+ import { Label } from '@/components/ui/label';
9
+ import { Skeleton } from '@/components/ui/skeleton';
10
+ import { useProfile, useSyncedState, useUpdateProfile } from '@/hooks';
11
+
12
+ /**
13
+ * Profile page demonstrating Supabase database integration.
14
+ * Shows current user's profile with ability to edit the name.
15
+ */
16
+ export function ProfilePage() {
17
+ const { t } = useLingui();
18
+ const { profile, isLoading, error, refetch } = useProfile();
19
+ const updateProfile = useUpdateProfile();
20
+
21
+ const [isEditing, setIsEditing] = useState(false);
22
+ // Sync name with profile, blocking sync while editing
23
+ const [name, setName] = useSyncedState(profile?.full_name ?? '', isEditing);
24
+
25
+ // Extract error messages for i18n (avoids object property access in Trans)
26
+ const fetchErrorMessage = error?.message;
27
+ const updateErrorMessage = updateProfile.error?.message;
28
+
29
+ const handleEdit = () => {
30
+ setIsEditing(true);
31
+ };
32
+
33
+ const handleCancel = () => {
34
+ setName(profile?.full_name ?? '');
35
+ setIsEditing(false);
36
+ };
37
+
38
+ const handleSave = () => {
39
+ updateProfile.mutate(
40
+ { full_name: name },
41
+ {
42
+ onSuccess: () => {
43
+ setIsEditing(false);
44
+ },
45
+ },
46
+ );
47
+ };
48
+
49
+ const handleSubmit = (e: React.FormEvent) => {
50
+ e.preventDefault();
51
+ handleSave();
52
+ };
53
+
54
+ return (
55
+ <div className="container mx-auto max-w-lg px-4 py-8">
56
+ <SEO
57
+ title={t({ message: 'Profile', comment: 'Profile page title for SEO' })}
58
+ description={t({
59
+ message: 'Manage your profile information',
60
+ comment: 'Profile page meta description for SEO',
61
+ })}
62
+ />
63
+
64
+ <Card>
65
+ <CardHeader>
66
+ <CardTitle>
67
+ <Trans comment="Profile page card title">Your Profile</Trans>
68
+ </CardTitle>
69
+ <CardDescription>
70
+ <Trans comment="Profile page card description">Manage your profile information stored in Supabase</Trans>
71
+ </CardDescription>
72
+ </CardHeader>
73
+ <CardContent>
74
+ {isLoading && (
75
+ <div className="space-y-4">
76
+ <div className="flex items-center gap-4">
77
+ <Skeleton className="size-16 rounded-full" />
78
+ <div className="space-y-2">
79
+ <Skeleton className="h-4 w-32" />
80
+ <Skeleton className="h-4 w-48" />
81
+ </div>
82
+ </div>
83
+ <Skeleton className="h-10 w-full" />
84
+ </div>
85
+ )}
86
+
87
+ {error && (
88
+ <div className="space-y-4">
89
+ <p className="text-destructive text-sm" role="alert">
90
+ <Trans comment="Error message when profile fails to load">
91
+ Failed to load profile: {fetchErrorMessage}
92
+ </Trans>
93
+ </p>
94
+ <Button variant="outline" onClick={() => refetch()}>
95
+ <Trans comment="Retry button label">Try Again</Trans>
96
+ </Button>
97
+ </div>
98
+ )}
99
+
100
+ {profile && (
101
+ <div className="space-y-6">
102
+ {/* Avatar and Email */}
103
+ <div className="flex items-center gap-4">
104
+ {profile.avatar_url ? (
105
+ <img
106
+ src={profile.avatar_url}
107
+ alt={t({ message: 'Profile avatar', comment: 'Alt text for profile avatar image' })}
108
+ className="size-16 rounded-full object-cover"
109
+ />
110
+ ) : (
111
+ <div className="bg-muted flex size-16 items-center justify-center rounded-full">
112
+ <span className="text-muted-foreground text-xl">
113
+ {profile.full_name?.[0]?.toUpperCase() ?? profile.email[0].toUpperCase()}
114
+ </span>
115
+ </div>
116
+ )}
117
+ <div>
118
+ <p className="font-medium">{profile.full_name ?? profile.email}</p>
119
+ <p className="text-muted-foreground text-sm">{profile.email}</p>
120
+ </div>
121
+ </div>
122
+
123
+ {/* Name Editor */}
124
+ <div className="space-y-2">
125
+ <Label htmlFor="full-name">
126
+ <Trans comment="Full name field label">Full Name</Trans>
127
+ </Label>
128
+ {isEditing ? (
129
+ <form onSubmit={handleSubmit} className="space-y-2">
130
+ <Input
131
+ id="full-name"
132
+ value={name}
133
+ onChange={(e) => setName(e.target.value)}
134
+ placeholder={t({ message: 'Enter your name', comment: 'Name input placeholder' })}
135
+ />
136
+ <div className="flex gap-2">
137
+ <Button type="submit" disabled={updateProfile.isPending}>
138
+ {updateProfile.isPending ? (
139
+ <Trans comment="Save button loading state">Saving...</Trans>
140
+ ) : (
141
+ <Trans comment="Save button label">Save</Trans>
142
+ )}
143
+ </Button>
144
+ <Button type="button" variant="outline" onClick={handleCancel} disabled={updateProfile.isPending}>
145
+ <Trans comment="Cancel button label">Cancel</Trans>
146
+ </Button>
147
+ </div>
148
+ {updateProfile.error && (
149
+ <p className="text-destructive text-sm" role="alert">
150
+ <Trans comment="Error message when update fails">Failed to update: {updateErrorMessage}</Trans>
151
+ </p>
152
+ )}
153
+ </form>
154
+ ) : (
155
+ <div className="flex items-center gap-2">
156
+ <p className="text-muted-foreground">
157
+ {profile.full_name || <Trans comment="Placeholder when no name is set">Not set</Trans>}
158
+ </p>
159
+ <Button variant="ghost" size="sm" onClick={handleEdit}>
160
+ <Trans comment="Edit button label">Edit</Trans>
161
+ </Button>
162
+ </div>
163
+ )}
164
+ </div>
165
+ </div>
166
+ )}
167
+ </CardContent>
168
+ </Card>
169
+ </div>
170
+ );
171
+ }
@@ -1,2 +1,3 @@
1
1
  export { HomePage } from './Home';
2
2
  export { NotFoundPage } from './NotFound';
3
+ export { ProfilePage } from './Profile';
@@ -2,6 +2,7 @@ import { create } from 'zustand';
2
2
  import { devtools, persist } from 'zustand/middleware';
3
3
 
4
4
  import { createSelectors } from '@/lib/createSelectors';
5
+ import { env } from '@/lib/env';
5
6
  import { STORAGE_KEYS } from '@/lib/storageKeys';
6
7
 
7
8
  export type Theme = 'light' | 'dark' | 'system';
@@ -75,7 +76,7 @@ const usePreferencesStoreBase = create<PreferencesState>()(
75
76
  },
76
77
  },
77
78
  ),
78
- { name: 'preferences', enabled: process.env.NODE_ENV === 'development' },
79
+ { name: 'preferences', enabled: env.DEV },
79
80
  ),
80
81
  );
81
82
 
@@ -1,39 +1,67 @@
1
+ /**
2
+ * Clerk authentication mocks for testing.
3
+ *
4
+ * Provides mock implementations of Clerk components and hooks,
5
+ * with state controls for testing different auth scenarios.
6
+ */
7
+
1
8
  import type { ReactNode } from 'react';
2
9
 
10
+ import { MOCK_AUTH_TOKEN, MOCK_SESSION_ID } from '@/mocks/constants';
11
+ import { defaultUser, type MockUser } from '@/mocks/fixtures/users';
12
+
13
+ // Re-export user fixtures for test convenience
14
+ export { createUser, createUsers, defaultUser, mockUsers, type MockUser } from '@/mocks/fixtures/users';
15
+
3
16
  // =============================================================================
4
- // Mock State
17
+ // Types
5
18
  // =============================================================================
6
19
 
7
20
  interface ClerkMockState {
8
21
  isSignedIn: boolean;
9
22
  isLoaded: boolean;
23
+ user: MockUser;
10
24
  }
11
25
 
26
+ // =============================================================================
27
+ // Default State
28
+ // =============================================================================
29
+
12
30
  const defaultState: ClerkMockState = {
13
31
  isSignedIn: true,
14
32
  isLoaded: true,
33
+ user: defaultUser,
15
34
  };
16
35
 
17
36
  let mockState: ClerkMockState = { ...defaultState };
18
37
 
19
38
  // =============================================================================
20
- // Test Utilities
39
+ // State Controls
21
40
  // =============================================================================
22
41
 
23
- export function setMockSignedIn(value: boolean) {
42
+ /** Set whether the user is signed in */
43
+ export function setMockClerkSignedIn(value: boolean) {
24
44
  mockState.isSignedIn = value;
25
45
  }
26
46
 
27
- export function setMockLoaded(value: boolean) {
47
+ /** Set whether Clerk has finished loading */
48
+ export function setMockClerkLoaded(value: boolean) {
28
49
  mockState.isLoaded = value;
29
50
  }
30
51
 
52
+ /** Set multiple Clerk state values at once */
31
53
  export function setMockClerkState(state: Partial<ClerkMockState>) {
32
54
  mockState = { ...mockState, ...state };
33
55
  }
34
56
 
57
+ /** Set mock user properties */
58
+ export function setMockClerkUser(user: Partial<MockUser>) {
59
+ mockState.user = { ...mockState.user, ...user };
60
+ }
61
+
62
+ /** Reset all Clerk mocks to default state */
35
63
  export function resetClerkMocks() {
36
- mockState = { ...defaultState };
64
+ mockState = { ...defaultState, user: { ...defaultUser } };
37
65
  }
38
66
 
39
67
  // =============================================================================
@@ -83,15 +111,27 @@ export function useAuth() {
83
111
  return {
84
112
  isLoaded: mockState.isLoaded,
85
113
  isSignedIn: mockState.isSignedIn,
86
- userId: mockState.isSignedIn ? 'user_123' : null,
87
- sessionId: mockState.isSignedIn ? 'sess_123' : null,
88
- getToken: async () => (mockState.isSignedIn ? 'mock-token' : null),
114
+ userId: mockState.isSignedIn ? mockState.user.id : null,
115
+ sessionId: mockState.isSignedIn ? MOCK_SESSION_ID : null,
116
+ getToken: async () => (mockState.isSignedIn ? MOCK_AUTH_TOKEN : null),
89
117
  };
90
118
  }
91
119
 
92
120
  export function useUser() {
93
121
  return {
94
122
  isLoaded: mockState.isLoaded,
95
- user: mockState.isSignedIn ? { id: 'user_123', firstName: 'Test', lastName: 'User' } : null,
123
+ user: mockState.isSignedIn ? mockState.user : null,
124
+ };
125
+ }
126
+
127
+ export function useSession() {
128
+ return {
129
+ isLoaded: mockState.isLoaded,
130
+ session: mockState.isSignedIn
131
+ ? {
132
+ id: MOCK_SESSION_ID,
133
+ getToken: async () => MOCK_AUTH_TOKEN,
134
+ }
135
+ : null,
96
136
  };
97
137
  }
@@ -0,0 +1,58 @@
1
+ import { vi } from 'vitest';
2
+
3
+ /**
4
+ * Mock a successful fetch response with JSON data.
5
+ *
6
+ * @remarks Requires `vi.spyOn(global, 'fetch')` in beforeEach
7
+ */
8
+ export function mockFetchSuccess<T>(data: T, status = 200): void {
9
+ vi.mocked(global.fetch).mockResolvedValueOnce({
10
+ ok: status >= 200 && status < 300,
11
+ status,
12
+ json: () => Promise.resolve(data),
13
+ } as Response);
14
+ }
15
+
16
+ /**
17
+ * Mock a 204 No Content response.
18
+ *
19
+ * @remarks Requires `vi.spyOn(global, 'fetch')` in beforeEach
20
+ */
21
+ export function mockFetchNoContent(): void {
22
+ vi.mocked(global.fetch).mockResolvedValueOnce({
23
+ ok: true,
24
+ status: 204,
25
+ } as Response);
26
+ }
27
+
28
+ /**
29
+ * Mock an error response (4xx/5xx).
30
+ *
31
+ * @remarks Requires `vi.spyOn(global, 'fetch')` in beforeEach
32
+ */
33
+ export function mockFetchError(status: number, message: string, hasJson = true): void {
34
+ vi.mocked(global.fetch).mockResolvedValueOnce({
35
+ ok: false,
36
+ status,
37
+ statusText: message,
38
+ json: hasJson ? () => Promise.resolve({ message }) : () => Promise.reject(new Error('No JSON')),
39
+ } as Response);
40
+ }
41
+
42
+ /**
43
+ * Mock a network error (fetch rejection).
44
+ *
45
+ * @remarks Requires `vi.spyOn(global, 'fetch')` in beforeEach
46
+ */
47
+ export function mockFetchNetworkError(message = 'Network failure'): void {
48
+ vi.mocked(global.fetch).mockRejectedValueOnce(new Error(message));
49
+ }
50
+
51
+ /**
52
+ * Mock an unknown error (non-Error rejection).
53
+ *
54
+ * @remarks Requires `vi.spyOn(global, 'fetch')` in beforeEach
55
+ */
56
+ export function mockFetchUnknownError(rejection: unknown = 'string error'): void {
57
+ vi.mocked(global.fetch).mockRejectedValueOnce(rejection);
58
+ }
@@ -1,11 +1,57 @@
1
1
  // Custom render with all providers
2
2
  export { createTestQueryClient, render } from './providers';
3
3
 
4
- // Mock utilities
5
- export { mockMatchMedia } from './mocks';
4
+ // Browser API mocks
5
+ export {
6
+ mockAnimationFrame,
7
+ mockMatchMedia,
8
+ mockScrollTo,
9
+ mockStorageRemoveItemError,
10
+ mockStorageSetItemError,
11
+ silenceConsoleError,
12
+ silenceConsoleLog,
13
+ silenceConsoleWarn,
14
+ } from './mocks';
6
15
 
7
16
  // Clerk test utilities
8
- export { resetClerkMocks, setMockClerkState, setMockLoaded, setMockSignedIn } from './clerkMock';
17
+ export {
18
+ createUser,
19
+ createUsers,
20
+ mockUsers,
21
+ resetClerkMocks,
22
+ setMockClerkLoaded,
23
+ setMockClerkSignedIn,
24
+ setMockClerkState,
25
+ setMockClerkUser,
26
+ type MockUser,
27
+ } from './clerkMock';
28
+
29
+ // Supabase test utilities
30
+ export {
31
+ createMockSupabaseClient,
32
+ createProfile,
33
+ createProfiles,
34
+ mockProfiles,
35
+ resetSupabaseMocks,
36
+ setMockSupabaseData,
37
+ setMockSupabaseError,
38
+ type Profile,
39
+ } from './supabaseMock';
40
+
41
+ // Fetch mock utilities
42
+ export {
43
+ mockFetchError,
44
+ mockFetchNetworkError,
45
+ mockFetchNoContent,
46
+ mockFetchSuccess,
47
+ mockFetchUnknownError,
48
+ } from './fetchMock';
9
49
 
10
50
  // MSW server instance
11
51
  export { server } from '@/mocks/node';
52
+
53
+ // Todo fixtures
54
+ export { createTodo, createTodos, mockTodos, type Todo } from '@/mocks/fixtures/todos';
55
+
56
+ // Shared test constants
57
+ export { MOCK_AUTH_TOKEN, MOCK_SESSION_ID, MOCK_SUPABASE_URL, MOCK_TIMESTAMPS, MOCK_USER } from '@/mocks/constants';
@@ -1,8 +1,25 @@
1
+ /**
2
+ * Browser API mocks for testing.
3
+ *
4
+ * Provides reusable mock implementations for browser APIs
5
+ * that are commonly needed across tests.
6
+ */
7
+
1
8
  import { vi } from 'vitest';
2
9
 
10
+ // =============================================================================
11
+ // Media Query Mocks
12
+ // =============================================================================
13
+
3
14
  /**
4
15
  * Creates a mock for window.matchMedia.
5
- * Usage: window.matchMedia = mockMatchMedia(true) // matches
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * beforeEach(() => {
20
+ * window.matchMedia = mockMatchMedia(true); // matches query
21
+ * });
22
+ * ```
6
23
  */
7
24
  export const mockMatchMedia = (matches: boolean) =>
8
25
  vi.fn().mockImplementation((query: string) => ({
@@ -15,3 +32,113 @@ export const mockMatchMedia = (matches: boolean) =>
15
32
  removeListener: vi.fn(),
16
33
  dispatchEvent: vi.fn(),
17
34
  }));
35
+
36
+ // =============================================================================
37
+ // Console Mocks
38
+ // =============================================================================
39
+
40
+ /**
41
+ * Silence console.error during a test.
42
+ * Returns a spy that can be restored with `.mockRestore()`.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * it('handles error gracefully', () => {
47
+ * const spy = silenceConsoleError();
48
+ * // ... test that triggers console.error
49
+ * spy.mockRestore();
50
+ * });
51
+ * ```
52
+ */
53
+ export function silenceConsoleError() {
54
+ return vi.spyOn(console, 'error').mockImplementation(() => {});
55
+ }
56
+
57
+ /**
58
+ * Silence console.warn during a test.
59
+ */
60
+ export function silenceConsoleWarn() {
61
+ return vi.spyOn(console, 'warn').mockImplementation(() => {});
62
+ }
63
+
64
+ /**
65
+ * Silence console.log during a test.
66
+ */
67
+ export function silenceConsoleLog() {
68
+ return vi.spyOn(console, 'log').mockImplementation(() => {});
69
+ }
70
+
71
+ // =============================================================================
72
+ // Animation Frame Mocks
73
+ // =============================================================================
74
+
75
+ /**
76
+ * Creates mocks for requestAnimationFrame and cancelAnimationFrame.
77
+ * Returns the callback capture for manual triggering.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * let rafCallback: FrameRequestCallback | null = null;
82
+ * beforeEach(() => {
83
+ * rafCallback = mockAnimationFrame();
84
+ * });
85
+ *
86
+ * it('updates on animation frame', () => {
87
+ * // trigger state change
88
+ * act(() => rafCallback?.(0));
89
+ * // assert new state
90
+ * });
91
+ * ```
92
+ */
93
+ export function mockAnimationFrame() {
94
+ let callback: FrameRequestCallback | null = null;
95
+
96
+ vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
97
+ callback = cb;
98
+ return 1;
99
+ });
100
+ vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {});
101
+
102
+ return () => callback;
103
+ }
104
+
105
+ // =============================================================================
106
+ // Scroll Mocks
107
+ // =============================================================================
108
+
109
+ /**
110
+ * Mock window.scrollTo for tests.
111
+ * Returns a spy for assertions.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const scrollSpy = mockScrollTo();
116
+ * // trigger scroll
117
+ * expect(scrollSpy).toHaveBeenCalledWith(0, 0);
118
+ * ```
119
+ */
120
+ export function mockScrollTo() {
121
+ return vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
122
+ }
123
+
124
+ // =============================================================================
125
+ // Storage Mocks
126
+ // =============================================================================
127
+
128
+ /**
129
+ * Mock localStorage.setItem to throw (simulate quota exceeded).
130
+ */
131
+ export function mockStorageSetItemError() {
132
+ return vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
133
+ throw new Error('QuotaExceeded');
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Mock localStorage.removeItem to throw.
139
+ */
140
+ export function mockStorageRemoveItemError() {
141
+ return vi.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => {
142
+ throw new Error('Storage error');
143
+ });
144
+ }
@@ -8,6 +8,7 @@ import { MemoryRouter } from 'react-router';
8
8
  import { ClerkThemeProvider } from '@/contexts/clerkContext';
9
9
  import { MobileProvider } from '@/contexts/mobileContext';
10
10
  import { PerformanceProviderWrapper } from '@/contexts/performanceContext';
11
+ import { SupabaseProvider } from '@/contexts/supabaseContext';
11
12
 
12
13
  // Setup empty English catalog for tests
13
14
  i18n.loadAndActivate({ locale: 'en', messages: {} });
@@ -38,15 +39,17 @@ interface WrapperProps {
38
39
  function AllProviders({ children }: WrapperProps) {
39
40
  const queryClient = createTestQueryClient();
40
41
 
41
- // Provider order matches main.tsx: Query > I18n > Router > Clerk > Mobile > Performance
42
+ // Provider order matches main.tsx: Query > I18n > Router > Clerk > Supabase > Mobile > Performance
42
43
  return (
43
44
  <QueryClientProvider client={queryClient}>
44
45
  <I18nProvider i18n={i18n}>
45
46
  <MemoryRouter>
46
47
  <ClerkThemeProvider publishableKey="test_key">
47
- <MobileProvider>
48
- <PerformanceProviderWrapper>{children}</PerformanceProviderWrapper>
49
- </MobileProvider>
48
+ <SupabaseProvider>
49
+ <MobileProvider>
50
+ <PerformanceProviderWrapper>{children}</PerformanceProviderWrapper>
51
+ </MobileProvider>
52
+ </SupabaseProvider>
50
53
  </ClerkThemeProvider>
51
54
  </MemoryRouter>
52
55
  </I18nProvider>