@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.
- package/README.md +2 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -1
- package/dist/features/definitions/auth.d.ts +3 -0
- package/dist/features/definitions/auth.d.ts.map +1 -0
- package/dist/features/definitions/auth.js +17 -0
- package/dist/features/definitions/auth.js.map +1 -0
- package/dist/features/definitions/core.d.ts.map +1 -1
- package/dist/features/definitions/core.js +16 -1
- package/dist/features/definitions/core.js.map +1 -1
- package/dist/features/definitions/database.d.ts +3 -0
- package/dist/features/definitions/database.d.ts.map +1 -0
- package/dist/features/definitions/database.js +45 -0
- package/dist/features/definitions/database.js.map +1 -0
- package/dist/features/definitions/deployment.d.ts +3 -0
- package/dist/features/definitions/deployment.d.ts.map +1 -0
- package/dist/features/definitions/deployment.js +14 -0
- package/dist/features/definitions/deployment.js.map +1 -0
- package/dist/features/definitions/forms.d.ts.map +1 -1
- package/dist/features/definitions/forms.js +4 -0
- package/dist/features/definitions/forms.js.map +1 -1
- package/dist/features/definitions/index.d.ts +3 -0
- package/dist/features/definitions/index.d.ts.map +1 -1
- package/dist/features/definitions/index.js +3 -0
- package/dist/features/definitions/index.js.map +1 -1
- package/dist/features/definitions/mobile.d.ts.map +1 -1
- package/dist/features/definitions/mobile.js +11 -2
- package/dist/features/definitions/mobile.js.map +1 -1
- package/dist/features/definitions/observability.js +1 -1
- package/dist/features/definitions/observability.js.map +1 -1
- package/dist/features/definitions/routing.d.ts.map +1 -1
- package/dist/features/definitions/routing.js +2 -1
- package/dist/features/definitions/routing.js.map +1 -1
- package/dist/features/definitions/state.d.ts.map +1 -1
- package/dist/features/definitions/state.js +9 -2
- package/dist/features/definitions/state.js.map +1 -1
- package/dist/features/definitions/testing.d.ts.map +1 -1
- package/dist/features/definitions/testing.js +4 -2
- package/dist/features/definitions/testing.js.map +1 -1
- package/dist/features/registry.d.ts.map +1 -1
- package/dist/features/registry.js +4 -1
- package/dist/features/registry.js.map +1 -1
- package/dist/features/types.test.js +6 -2
- package/dist/features/types.test.js.map +1 -1
- package/dist/resources/docs.d.ts.map +1 -1
- package/dist/resources/docs.js +5 -0
- package/dist/resources/docs.js.map +1 -1
- package/dist/tools/add-features.js +1 -1
- package/dist/tools/add-features.js.map +1 -1
- package/dist/utils/docs.d.ts.map +1 -1
- package/dist/utils/docs.js +2 -0
- package/dist/utils/docs.js.map +1 -1
- package/dist/utils/scaffold/claude-md/index.d.ts.map +1 -1
- package/dist/utils/scaffold/claude-md/index.js +3 -1
- package/dist/utils/scaffold/claude-md/index.js.map +1 -1
- package/dist/utils/scaffold/claude-md/sections.d.ts +2 -0
- package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
- package/dist/utils/scaffold/claude-md/sections.js +132 -2
- package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
- package/dist/utils/scaffold/compute.js +1 -1
- package/dist/utils/scaffold/compute.js.map +1 -1
- package/dist/utils/scaffold/generators.d.ts +2 -2
- package/dist/utils/scaffold/generators.d.ts.map +1 -1
- package/dist/utils/scaffold/generators.js +64 -22
- package/dist/utils/scaffold/generators.js.map +1 -1
- package/package.json +1 -1
- package/templates/.env.example +44 -10
- package/templates/.github/workflows/ci.yml +12 -4
- package/templates/.github/workflows/deploy.yml +59 -0
- package/templates/CLAUDE.md +251 -2
- package/templates/docs/ARCHITECTURE.md +13 -12
- package/templates/docs/AUTHENTICATION.md +325 -0
- package/templates/docs/CODING_STANDARDS.md +65 -0
- package/templates/docs/DEPLOYMENT.md +268 -0
- package/templates/docs/E2E_TESTING.md +133 -11
- package/templates/docs/SUPABASE_INTEGRATION.md +310 -0
- package/templates/docs/TESTING.md +195 -77
- package/templates/e2e/auth/auth.setup.ts +60 -0
- package/templates/e2e/fixtures/index.ts +24 -2
- package/templates/e2e/tests/profile.auth.spec.ts +103 -0
- package/templates/e2e/tests/profile.spec.ts +64 -0
- package/templates/e2e/tests/register-form.spec.ts +38 -0
- package/templates/gitignore +5 -0
- package/templates/package.json +15 -3
- package/templates/playwright.config.ts +39 -4
- package/templates/src/App.tsx +32 -19
- package/templates/src/components/layout/Header.test.tsx +17 -1
- package/templates/src/components/layout/Header.tsx +13 -1
- package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +30 -0
- package/templates/src/components/shared/AccountButton/AccountButton.tsx +38 -0
- package/templates/src/components/shared/AccountButton/index.ts +1 -0
- package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.test.tsx +4 -4
- package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +55 -53
- package/templates/src/components/shared/ProfileSync/ProfileSync.test.tsx +44 -0
- package/templates/src/components/shared/ProfileSync/ProfileSync.tsx +104 -0
- package/templates/src/components/shared/ProfileSync/index.ts +1 -0
- package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.test.tsx +43 -0
- package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.tsx +35 -0
- package/templates/src/components/shared/ProtectedRoute/index.ts +1 -0
- package/templates/src/components/shared/index.ts +5 -2
- package/templates/src/contexts/clerkContext.tsx +45 -0
- package/templates/src/contexts/performanceContext.tsx +3 -3
- package/templates/src/contexts/supabaseContext.test.tsx +59 -0
- package/templates/src/contexts/supabaseContext.tsx +87 -0
- package/templates/src/hooks/index.ts +40 -2
- package/templates/src/hooks/supabase/index.ts +12 -0
- package/templates/src/hooks/supabase/useProfiles.test.tsx +207 -0
- package/templates/src/hooks/supabase/useProfiles.ts +213 -0
- package/templates/src/hooks/supabase/useSupabaseQuery.test.tsx +150 -0
- package/templates/src/hooks/supabase/useSupabaseQuery.ts +91 -0
- package/templates/src/hooks/useCopyFeedback.test.ts +129 -0
- package/templates/src/hooks/useCopyFeedback.ts +41 -0
- package/templates/src/hooks/useDebouncedCallback.test.ts +164 -0
- package/templates/src/hooks/useDebouncedCallback.ts +47 -0
- package/templates/src/hooks/useDocumentTitle.test.ts +59 -0
- package/templates/src/hooks/useDocumentTitle.ts +31 -0
- package/templates/src/hooks/useIOSViewportReset.test.ts +58 -0
- package/templates/src/hooks/useIOSViewportReset.ts +18 -0
- package/templates/src/hooks/useKeyboardShortcut.test.ts +86 -0
- package/templates/src/hooks/useKeyboardShortcuts.ts +44 -0
- package/templates/src/hooks/useLocalStorage.test.ts +111 -0
- package/templates/src/hooks/useLocalStorage.ts +77 -0
- package/templates/src/hooks/useSyncedFormData.test.ts +75 -0
- package/templates/src/hooks/useSyncedFormData.ts +21 -0
- package/templates/src/hooks/useSyncedState.test.ts +119 -0
- package/templates/src/hooks/useSyncedState.ts +30 -0
- package/templates/src/index.css +1 -0
- package/templates/src/lib/api.test.ts +30 -38
- package/templates/src/lib/api.ts +1 -7
- package/templates/src/lib/config.ts +54 -4
- package/templates/src/lib/constants.ts +10 -0
- package/templates/src/lib/createSelectors.test.ts +136 -0
- package/templates/src/lib/createSelectors.ts +31 -0
- package/templates/src/lib/env.ts +36 -14
- package/templates/src/lib/index.ts +5 -2
- package/templates/src/lib/routes.ts +1 -0
- package/templates/src/lib/sentry.ts +58 -0
- package/templates/src/lib/storage.ts +6 -2
- package/templates/src/lib/supabase/client.ts +58 -0
- package/templates/src/lib/supabase/index.ts +5 -0
- package/templates/src/main.tsx +19 -31
- package/templates/src/mocks/constants.ts +31 -0
- package/templates/src/mocks/fixtures/index.ts +3 -1
- package/templates/src/mocks/fixtures/profiles.ts +55 -0
- package/templates/src/mocks/fixtures/users.ts +91 -0
- package/templates/src/mocks/handlers/index.ts +2 -1
- package/templates/src/mocks/handlers/supabase.ts +64 -0
- package/templates/src/mocks/handlers/todos.ts +1 -1
- package/templates/src/mocks/index.ts +6 -0
- package/templates/src/pages/Profile.test.tsx +263 -0
- package/templates/src/pages/Profile.tsx +171 -0
- package/templates/src/pages/index.ts +1 -0
- package/templates/src/stores/preferencesStore.ts +35 -9
- package/templates/src/test/clerkMock.tsx +137 -0
- package/templates/src/test/fetchMock.ts +58 -0
- package/templates/src/test/index.ts +51 -2
- package/templates/src/test/mocks.ts +128 -1
- package/templates/src/test/providers.tsx +10 -4
- package/templates/src/test/supabaseMock.ts +112 -0
- package/templates/src/test-setup.ts +42 -2
- package/templates/src/types/database.ts +46 -0
- package/templates/src/types/index.ts +1 -0
- package/templates/src/types/supabase.ts +167 -0
- package/templates/src/vite-env.d.ts +6 -0
- package/templates/supabase/migrations/20260104000000_create_profiles_table.sql +67 -0
- 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
|
-
//
|
|
5
|
-
export {
|
|
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
|
-
*
|
|
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
|
-
<
|
|
46
|
-
<
|
|
47
|
-
|
|
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'>;
|