@react-spa-scaffold/mcp 2.1.1 → 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 (168) hide show
  1. package/README.md +2 -1
  2. package/dist/constants.d.ts +4 -0
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +4 -0
  5. package/dist/constants.js.map +1 -1
  6. package/dist/features/definitions/auth.d.ts +3 -0
  7. package/dist/features/definitions/auth.d.ts.map +1 -0
  8. package/dist/features/definitions/auth.js +17 -0
  9. package/dist/features/definitions/auth.js.map +1 -0
  10. package/dist/features/definitions/core.d.ts.map +1 -1
  11. package/dist/features/definitions/core.js +16 -1
  12. package/dist/features/definitions/core.js.map +1 -1
  13. package/dist/features/definitions/database.d.ts +3 -0
  14. package/dist/features/definitions/database.d.ts.map +1 -0
  15. package/dist/features/definitions/database.js +45 -0
  16. package/dist/features/definitions/database.js.map +1 -0
  17. package/dist/features/definitions/deployment.d.ts +3 -0
  18. package/dist/features/definitions/deployment.d.ts.map +1 -0
  19. package/dist/features/definitions/deployment.js +14 -0
  20. package/dist/features/definitions/deployment.js.map +1 -0
  21. package/dist/features/definitions/forms.d.ts.map +1 -1
  22. package/dist/features/definitions/forms.js +4 -0
  23. package/dist/features/definitions/forms.js.map +1 -1
  24. package/dist/features/definitions/index.d.ts +3 -0
  25. package/dist/features/definitions/index.d.ts.map +1 -1
  26. package/dist/features/definitions/index.js +3 -0
  27. package/dist/features/definitions/index.js.map +1 -1
  28. package/dist/features/definitions/mobile.d.ts.map +1 -1
  29. package/dist/features/definitions/mobile.js +11 -2
  30. package/dist/features/definitions/mobile.js.map +1 -1
  31. package/dist/features/definitions/observability.js +1 -1
  32. package/dist/features/definitions/observability.js.map +1 -1
  33. package/dist/features/definitions/routing.d.ts.map +1 -1
  34. package/dist/features/definitions/routing.js +2 -1
  35. package/dist/features/definitions/routing.js.map +1 -1
  36. package/dist/features/definitions/state.d.ts.map +1 -1
  37. package/dist/features/definitions/state.js +9 -2
  38. package/dist/features/definitions/state.js.map +1 -1
  39. package/dist/features/definitions/testing.d.ts.map +1 -1
  40. package/dist/features/definitions/testing.js +4 -2
  41. package/dist/features/definitions/testing.js.map +1 -1
  42. package/dist/features/registry.d.ts.map +1 -1
  43. package/dist/features/registry.js +4 -1
  44. package/dist/features/registry.js.map +1 -1
  45. package/dist/features/types.test.js +6 -2
  46. package/dist/features/types.test.js.map +1 -1
  47. package/dist/resources/docs.d.ts.map +1 -1
  48. package/dist/resources/docs.js +5 -0
  49. package/dist/resources/docs.js.map +1 -1
  50. package/dist/tools/add-features.js +1 -1
  51. package/dist/tools/add-features.js.map +1 -1
  52. package/dist/utils/docs.d.ts.map +1 -1
  53. package/dist/utils/docs.js +2 -0
  54. package/dist/utils/docs.js.map +1 -1
  55. package/dist/utils/scaffold/claude-md/index.d.ts.map +1 -1
  56. package/dist/utils/scaffold/claude-md/index.js +3 -1
  57. package/dist/utils/scaffold/claude-md/index.js.map +1 -1
  58. package/dist/utils/scaffold/claude-md/sections.d.ts +2 -0
  59. package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
  60. package/dist/utils/scaffold/claude-md/sections.js +132 -2
  61. package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
  62. package/dist/utils/scaffold/compute.js +1 -1
  63. package/dist/utils/scaffold/compute.js.map +1 -1
  64. package/dist/utils/scaffold/generators.d.ts +2 -2
  65. package/dist/utils/scaffold/generators.d.ts.map +1 -1
  66. package/dist/utils/scaffold/generators.js +64 -22
  67. package/dist/utils/scaffold/generators.js.map +1 -1
  68. package/package.json +1 -1
  69. package/templates/.env.example +44 -10
  70. package/templates/.github/workflows/ci.yml +12 -4
  71. package/templates/.github/workflows/deploy.yml +59 -0
  72. package/templates/CLAUDE.md +251 -2
  73. package/templates/docs/ARCHITECTURE.md +13 -12
  74. package/templates/docs/AUTHENTICATION.md +325 -0
  75. package/templates/docs/CODING_STANDARDS.md +65 -0
  76. package/templates/docs/DEPLOYMENT.md +268 -0
  77. package/templates/docs/E2E_TESTING.md +133 -11
  78. package/templates/docs/SUPABASE_INTEGRATION.md +310 -0
  79. package/templates/docs/TESTING.md +195 -77
  80. package/templates/e2e/auth/auth.setup.ts +60 -0
  81. package/templates/e2e/fixtures/index.ts +24 -2
  82. package/templates/e2e/tests/profile.auth.spec.ts +103 -0
  83. package/templates/e2e/tests/profile.spec.ts +64 -0
  84. package/templates/e2e/tests/register-form.spec.ts +38 -0
  85. package/templates/gitignore +5 -0
  86. package/templates/package.json +15 -3
  87. package/templates/playwright.config.ts +39 -4
  88. package/templates/src/App.tsx +32 -19
  89. package/templates/src/components/layout/Header.test.tsx +17 -1
  90. package/templates/src/components/layout/Header.tsx +13 -1
  91. package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +30 -0
  92. package/templates/src/components/shared/AccountButton/AccountButton.tsx +38 -0
  93. package/templates/src/components/shared/AccountButton/index.ts +1 -0
  94. package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.test.tsx +4 -4
  95. package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +55 -53
  96. package/templates/src/components/shared/ProfileSync/ProfileSync.test.tsx +44 -0
  97. package/templates/src/components/shared/ProfileSync/ProfileSync.tsx +104 -0
  98. package/templates/src/components/shared/ProfileSync/index.ts +1 -0
  99. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.test.tsx +43 -0
  100. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.tsx +35 -0
  101. package/templates/src/components/shared/ProtectedRoute/index.ts +1 -0
  102. package/templates/src/components/shared/index.ts +5 -2
  103. package/templates/src/contexts/clerkContext.tsx +45 -0
  104. package/templates/src/contexts/performanceContext.tsx +3 -3
  105. package/templates/src/contexts/supabaseContext.test.tsx +59 -0
  106. package/templates/src/contexts/supabaseContext.tsx +87 -0
  107. package/templates/src/hooks/index.ts +40 -2
  108. package/templates/src/hooks/supabase/index.ts +12 -0
  109. package/templates/src/hooks/supabase/useProfiles.test.tsx +207 -0
  110. package/templates/src/hooks/supabase/useProfiles.ts +213 -0
  111. package/templates/src/hooks/supabase/useSupabaseQuery.test.tsx +150 -0
  112. package/templates/src/hooks/supabase/useSupabaseQuery.ts +91 -0
  113. package/templates/src/hooks/useCopyFeedback.test.ts +129 -0
  114. package/templates/src/hooks/useCopyFeedback.ts +41 -0
  115. package/templates/src/hooks/useDebouncedCallback.test.ts +164 -0
  116. package/templates/src/hooks/useDebouncedCallback.ts +47 -0
  117. package/templates/src/hooks/useDocumentTitle.test.ts +59 -0
  118. package/templates/src/hooks/useDocumentTitle.ts +31 -0
  119. package/templates/src/hooks/useIOSViewportReset.test.ts +58 -0
  120. package/templates/src/hooks/useIOSViewportReset.ts +18 -0
  121. package/templates/src/hooks/useKeyboardShortcut.test.ts +86 -0
  122. package/templates/src/hooks/useKeyboardShortcuts.ts +44 -0
  123. package/templates/src/hooks/useLocalStorage.test.ts +111 -0
  124. package/templates/src/hooks/useLocalStorage.ts +77 -0
  125. package/templates/src/hooks/useSyncedFormData.test.ts +75 -0
  126. package/templates/src/hooks/useSyncedFormData.ts +21 -0
  127. package/templates/src/hooks/useSyncedState.test.ts +119 -0
  128. package/templates/src/hooks/useSyncedState.ts +30 -0
  129. package/templates/src/index.css +1 -0
  130. package/templates/src/lib/api.test.ts +30 -38
  131. package/templates/src/lib/api.ts +1 -7
  132. package/templates/src/lib/config.ts +54 -4
  133. package/templates/src/lib/constants.ts +10 -0
  134. package/templates/src/lib/createSelectors.test.ts +136 -0
  135. package/templates/src/lib/createSelectors.ts +31 -0
  136. package/templates/src/lib/env.ts +36 -14
  137. package/templates/src/lib/index.ts +5 -2
  138. package/templates/src/lib/routes.ts +1 -0
  139. package/templates/src/lib/sentry.ts +58 -0
  140. package/templates/src/lib/storage.ts +6 -2
  141. package/templates/src/lib/supabase/client.ts +58 -0
  142. package/templates/src/lib/supabase/index.ts +5 -0
  143. package/templates/src/main.tsx +19 -31
  144. package/templates/src/mocks/constants.ts +31 -0
  145. package/templates/src/mocks/fixtures/index.ts +3 -1
  146. package/templates/src/mocks/fixtures/profiles.ts +55 -0
  147. package/templates/src/mocks/fixtures/users.ts +91 -0
  148. package/templates/src/mocks/handlers/index.ts +2 -1
  149. package/templates/src/mocks/handlers/supabase.ts +64 -0
  150. package/templates/src/mocks/handlers/todos.ts +1 -1
  151. package/templates/src/mocks/index.ts +6 -0
  152. package/templates/src/pages/Profile.test.tsx +263 -0
  153. package/templates/src/pages/Profile.tsx +171 -0
  154. package/templates/src/pages/index.ts +1 -0
  155. package/templates/src/stores/preferencesStore.ts +35 -9
  156. package/templates/src/test/clerkMock.tsx +137 -0
  157. package/templates/src/test/fetchMock.ts +58 -0
  158. package/templates/src/test/index.ts +51 -2
  159. package/templates/src/test/mocks.ts +128 -1
  160. package/templates/src/test/providers.tsx +10 -4
  161. package/templates/src/test/supabaseMock.ts +112 -0
  162. package/templates/src/test-setup.ts +42 -2
  163. package/templates/src/types/database.ts +46 -0
  164. package/templates/src/types/index.ts +1 -0
  165. package/templates/src/types/supabase.ts +167 -0
  166. package/templates/src/vite-env.d.ts +6 -0
  167. package/templates/supabase/migrations/20260104000000_create_profiles_table.sql +67 -0
  168. package/templates/vitest.config.ts +9 -1
@@ -0,0 +1,137 @@
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
+
8
+ import type { ReactNode } from 'react';
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
+
16
+ // =============================================================================
17
+ // Types
18
+ // =============================================================================
19
+
20
+ interface ClerkMockState {
21
+ isSignedIn: boolean;
22
+ isLoaded: boolean;
23
+ user: MockUser;
24
+ }
25
+
26
+ // =============================================================================
27
+ // Default State
28
+ // =============================================================================
29
+
30
+ const defaultState: ClerkMockState = {
31
+ isSignedIn: true,
32
+ isLoaded: true,
33
+ user: defaultUser,
34
+ };
35
+
36
+ let mockState: ClerkMockState = { ...defaultState };
37
+
38
+ // =============================================================================
39
+ // State Controls
40
+ // =============================================================================
41
+
42
+ /** Set whether the user is signed in */
43
+ export function setMockClerkSignedIn(value: boolean) {
44
+ mockState.isSignedIn = value;
45
+ }
46
+
47
+ /** Set whether Clerk has finished loading */
48
+ export function setMockClerkLoaded(value: boolean) {
49
+ mockState.isLoaded = value;
50
+ }
51
+
52
+ /** Set multiple Clerk state values at once */
53
+ export function setMockClerkState(state: Partial<ClerkMockState>) {
54
+ mockState = { ...mockState, ...state };
55
+ }
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 */
63
+ export function resetClerkMocks() {
64
+ mockState = { ...defaultState, user: { ...defaultUser } };
65
+ }
66
+
67
+ // =============================================================================
68
+ // Mock Components
69
+ // =============================================================================
70
+
71
+ export function SignedIn({ children }: { children: ReactNode }) {
72
+ return mockState.isLoaded && mockState.isSignedIn ? <>{children}</> : null;
73
+ }
74
+
75
+ export function SignedOut({ children }: { children: ReactNode }) {
76
+ return mockState.isLoaded && !mockState.isSignedIn ? <>{children}</> : null;
77
+ }
78
+
79
+ export function SignInButton({ children }: { children?: ReactNode; mode?: string }) {
80
+ return <div data-testid="sign-in-button">{children}</div>;
81
+ }
82
+
83
+ export function SignUpButton({ children }: { children?: ReactNode; mode?: string }) {
84
+ return <div data-testid="sign-up-button">{children}</div>;
85
+ }
86
+
87
+ export function UserButton() {
88
+ return <button data-testid="user-button">User</button>;
89
+ }
90
+
91
+ export function RedirectToSignIn() {
92
+ return <div data-testid="redirect-to-sign-in" />;
93
+ }
94
+
95
+ export function ClerkProvider({
96
+ children,
97
+ }: {
98
+ children: ReactNode;
99
+ publishableKey?: string;
100
+ afterSignOutUrl?: string;
101
+ appearance?: unknown;
102
+ }) {
103
+ return <>{children}</>;
104
+ }
105
+
106
+ // =============================================================================
107
+ // Mock Hooks
108
+ // =============================================================================
109
+
110
+ export function useAuth() {
111
+ return {
112
+ isLoaded: mockState.isLoaded,
113
+ isSignedIn: mockState.isSignedIn,
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),
117
+ };
118
+ }
119
+
120
+ export function useUser() {
121
+ return {
122
+ isLoaded: mockState.isLoaded,
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,
136
+ };
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,8 +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';
15
+
16
+ // Clerk test utilities
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';
6
49
 
7
50
  // MSW server instance
8
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
+ }
@@ -5,8 +5,10 @@ import { render, type RenderOptions } from '@testing-library/react';
5
5
  import { type ReactElement, type ReactNode } from 'react';
6
6
  import { MemoryRouter } from 'react-router';
7
7
 
8
+ import { ClerkThemeProvider } from '@/contexts/clerkContext';
8
9
  import { MobileProvider } from '@/contexts/mobileContext';
9
10
  import { PerformanceProviderWrapper } from '@/contexts/performanceContext';
11
+ import { SupabaseProvider } from '@/contexts/supabaseContext';
10
12
 
11
13
  // Setup empty English catalog for tests
12
14
  i18n.loadAndActivate({ locale: 'en', messages: {} });
@@ -37,14 +39,18 @@ interface WrapperProps {
37
39
  function AllProviders({ children }: WrapperProps) {
38
40
  const queryClient = createTestQueryClient();
39
41
 
40
- // Provider order matches main.tsx: Query > I18n > Router > Mobile > Performance
42
+ // Provider order matches main.tsx: Query > I18n > Router > Clerk > Supabase > Mobile > Performance
41
43
  return (
42
44
  <QueryClientProvider client={queryClient}>
43
45
  <I18nProvider i18n={i18n}>
44
46
  <MemoryRouter>
45
- <MobileProvider>
46
- <PerformanceProviderWrapper>{children}</PerformanceProviderWrapper>
47
- </MobileProvider>
47
+ <ClerkThemeProvider publishableKey="test_key">
48
+ <SupabaseProvider>
49
+ <MobileProvider>
50
+ <PerformanceProviderWrapper>{children}</PerformanceProviderWrapper>
51
+ </MobileProvider>
52
+ </SupabaseProvider>
53
+ </ClerkThemeProvider>
48
54
  </MemoryRouter>
49
55
  </I18nProvider>
50
56
  </QueryClientProvider>
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Supabase mocks for testing.
3
+ *
4
+ * Provides mock implementations of the Supabase context and client,
5
+ * with state controls for testing different scenarios.
6
+ */
7
+
8
+ import type { ReactNode } from 'react';
9
+ import { vi } from 'vitest';
10
+
11
+ // Re-export fixtures for test convenience
12
+ export { createProfile, createProfiles, mockProfiles } from '@/mocks/fixtures/profiles';
13
+ export type { Profile } from '@/types/database';
14
+
15
+ // =============================================================================
16
+ // Mock State
17
+ // =============================================================================
18
+
19
+ interface SupabaseMockState {
20
+ data: unknown[];
21
+ error: { message: string; code: string } | null;
22
+ }
23
+
24
+ const defaultState: SupabaseMockState = {
25
+ data: [],
26
+ error: null,
27
+ };
28
+
29
+ let mockState: SupabaseMockState = { ...defaultState };
30
+
31
+ // =============================================================================
32
+ // State Controls
33
+ // =============================================================================
34
+
35
+ /** Set mock data to be returned by Supabase queries */
36
+ export function setMockSupabaseData(data: unknown[]) {
37
+ mockState.data = data;
38
+ }
39
+
40
+ /** Set a mock error to be returned by Supabase queries */
41
+ export function setMockSupabaseError(error: { message: string; code: string } | null) {
42
+ mockState.error = error;
43
+ }
44
+
45
+ /** Reset all Supabase mocks to default state */
46
+ export function resetSupabaseMocks() {
47
+ mockState = { ...defaultState };
48
+ }
49
+
50
+ // =============================================================================
51
+ // Mock Query Builder
52
+ // =============================================================================
53
+
54
+ function createMockQueryBuilder() {
55
+ const resolveQuery = () => {
56
+ if (mockState.error) {
57
+ return { data: null, error: mockState.error };
58
+ }
59
+ return { data: mockState.data, error: null };
60
+ };
61
+
62
+ const resolveSingle = () => {
63
+ if (mockState.error) {
64
+ return { data: null, error: mockState.error };
65
+ }
66
+ return { data: mockState.data[0] ?? null, error: null };
67
+ };
68
+
69
+ return {
70
+ select: vi.fn().mockReturnThis(),
71
+ insert: vi.fn().mockReturnThis(),
72
+ update: vi.fn().mockReturnThis(),
73
+ delete: vi.fn().mockReturnThis(),
74
+ upsert: vi.fn().mockReturnThis(),
75
+ eq: vi.fn().mockReturnThis(),
76
+ neq: vi.fn().mockReturnThis(),
77
+ single: vi.fn().mockImplementation(() => Promise.resolve(resolveSingle())),
78
+ maybeSingle: vi.fn().mockImplementation(() => Promise.resolve(resolveSingle())),
79
+ then: (resolve: (value: { data: unknown[] | null; error: unknown }) => void) => {
80
+ resolve(resolveQuery());
81
+ return { catch: () => {} };
82
+ },
83
+ };
84
+ }
85
+
86
+ // =============================================================================
87
+ // Mock Supabase Client
88
+ // =============================================================================
89
+
90
+ /** Create a mock Supabase client for testing */
91
+ export function createMockSupabaseClient() {
92
+ return {
93
+ from: vi.fn().mockReturnValue(createMockQueryBuilder()),
94
+ };
95
+ }
96
+
97
+ // Singleton client instance for useSupabase hook (maintains referential equality)
98
+ const mockClientInstance = createMockSupabaseClient();
99
+
100
+ // =============================================================================
101
+ // Mock Context (for vi.mock)
102
+ // =============================================================================
103
+
104
+ /** Mock SupabaseProvider - passes through children */
105
+ export function SupabaseProvider({ children }: { children: ReactNode }) {
106
+ return children;
107
+ }
108
+
109
+ /** Mock useSupabase hook - returns stable client instance */
110
+ export function useSupabase() {
111
+ return mockClientInstance;
112
+ }
@@ -1,7 +1,45 @@
1
1
  import '@testing-library/jest-dom/vitest';
2
- import { afterAll, afterEach, beforeAll } from 'vitest';
2
+ import { afterAll, afterEach, beforeAll, vi } from 'vitest';
3
+
4
+ // =============================================================================
5
+ // Environment Variable Mock (must be mocked before any imports that use env.ts)
6
+ // =============================================================================
7
+ vi.mock('@/lib/env', () => ({
8
+ env: {
9
+ VITE_APP_NAME: 'My App',
10
+ VITE_APP_URL: 'http://localhost:5173',
11
+ VITE_API_URL: 'https://jsonplaceholder.typicode.com',
12
+ VITE_SENTRY_DSN: 'https://test@sentry.io/123',
13
+ VITE_SENTRY_ENABLED: false,
14
+ VITE_CLERK_PUBLISHABLE_KEY: 'pk_test_mock',
15
+ VITE_SUPABASE_DATABASE_URL: 'https://test.supabase.co',
16
+ VITE_SUPABASE_ANON_KEY: 'test-anon-key',
17
+ VITE_PERF_TEST: false,
18
+ MODE: 'test',
19
+ DEV: false,
20
+ PROD: false,
21
+ },
22
+ validateEnv: vi.fn(),
23
+ }));
3
24
 
4
25
  import { server } from '@/mocks/node';
26
+ import { resetClerkMocks } from '@/test/clerkMock';
27
+ import { resetSupabaseMocks } from '@/test/supabaseMock';
28
+
29
+ // =============================================================================
30
+ // Module Mocks
31
+ // =============================================================================
32
+
33
+ // Mock @clerk/react-router for testing
34
+ vi.mock('@clerk/react-router', async () => import('@/test/clerkMock'));
35
+
36
+ // Mock @clerk/themes for testing
37
+ vi.mock('@clerk/themes', () => ({
38
+ shadcn: { baseTheme: 'shadcn' },
39
+ }));
40
+
41
+ // Mock Supabase context with test client
42
+ vi.mock('@/contexts/supabaseContext', async () => import('@/test/supabaseMock'));
5
43
 
6
44
  // =============================================================================
7
45
  // MSW Server Setup
@@ -14,9 +52,11 @@ beforeAll(() => {
14
52
  });
15
53
  });
16
54
 
17
- // Reset handlers after each test to ensure test isolation
55
+ // Reset handlers and mocks after each test to ensure test isolation
18
56
  afterEach(() => {
19
57
  server.resetHandlers();
58
+ resetClerkMocks();
59
+ resetSupabaseMocks();
20
60
  });
21
61
 
22
62
  // Close MSW server after all tests complete
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Database type aliases and re-exports.
3
+ *
4
+ * This file provides a database-agnostic public API for type imports.
5
+ * The underlying types are auto-generated in supabase.ts via `npm run db:types`.
6
+ *
7
+ * Usage:
8
+ * import type { Profile, ProfileInsert, ProfileUpdate } from '@/types/database';
9
+ *
10
+ * When adding new tables:
11
+ * 1. Run `npm run db:types` to regenerate supabase.ts
12
+ * 2. Add convenience aliases here for your new tables
13
+ */
14
+
15
+ // Re-export everything from the auto-generated Supabase types
16
+ export * from './supabase';
17
+
18
+ // Re-import for creating aliases
19
+ import type { Database, Tables, TablesInsert, TablesUpdate } from './supabase';
20
+
21
+ // =============================================================================
22
+ // Generic Table Types
23
+ // =============================================================================
24
+
25
+ /**
26
+ * Table names available in the database schema.
27
+ */
28
+ export type TableName = keyof Database['public']['Tables'];
29
+
30
+ /**
31
+ * Row type for a given table.
32
+ */
33
+ export type TableRowType<T extends TableName> = Database['public']['Tables'][T]['Row'];
34
+
35
+ // =============================================================================
36
+ // Profile Types
37
+ // =============================================================================
38
+
39
+ /** Profile row type (read operations) */
40
+ export type Profile = Tables<'profiles'>;
41
+
42
+ /** Profile insert type (create operations) */
43
+ export type ProfileInsert = TablesInsert<'profiles'>;
44
+
45
+ /** Profile update type (update operations) */
46
+ export type ProfileUpdate = TablesUpdate<'profiles'>;
@@ -1,2 +1,3 @@
1
1
  export * from './preferences';
2
2
  export * from './api';
3
+ export * from './database';