@react-spa-scaffold/mcp 2.2.0 → 2.4.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/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/api.d.ts.map +1 -1
- package/dist/features/definitions/api.js +2 -1
- package/dist/features/definitions/api.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/electron.d.ts +3 -0
- package/dist/features/definitions/electron.d.ts.map +1 -0
- package/dist/features/definitions/electron.js +23 -0
- package/dist/features/definitions/electron.js.map +1 -0
- 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/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.d.ts +1 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/features/types.test.js +5 -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/tools/get-features.test.js +7 -0
- package/dist/tools/get-features.test.js.map +1 -1
- package/dist/tools/get-scaffold.d.ts +1 -0
- package/dist/tools/get-scaffold.d.ts.map +1 -1
- package/dist/tools/get-scaffold.js +4 -1
- package/dist/tools/get-scaffold.js.map +1 -1
- package/dist/tools/get-scaffold.test.js +50 -0
- package/dist/tools/get-scaffold.test.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 +4 -1
- package/dist/utils/scaffold/claude-md/index.js.map +1 -1
- package/dist/utils/scaffold/claude-md/sections.d.ts +3 -0
- package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
- package/dist/utils/scaffold/claude-md/sections.js +174 -2
- package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
- package/dist/utils/scaffold/compute.d.ts.map +1 -1
- package/dist/utils/scaffold/compute.js +4 -2
- package/dist/utils/scaffold/compute.js.map +1 -1
- package/dist/utils/scaffold/generators.d.ts +7 -2
- package/dist/utils/scaffold/generators.d.ts.map +1 -1
- package/dist/utils/scaffold/generators.js +100 -22
- package/dist/utils/scaffold/generators.js.map +1 -1
- package/package.json +1 -1
- package/templates/.env.example +40 -12
- package/templates/.github/workflows/ci.yml +49 -2
- package/templates/.github/workflows/deploy.yml +46 -0
- package/templates/CLAUDE.md +180 -1
- package/templates/docs/AUTHENTICATION.md +325 -0
- package/templates/docs/DEPLOYMENT.md +296 -0
- package/templates/docs/E2E_TESTING.md +81 -4
- 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 +11 -0
- 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/forge.config.js +53 -0
- package/templates/gitignore +5 -0
- package/templates/package.json +13 -1
- package/templates/playwright.config.ts +33 -3
- package/templates/src/App.tsx +32 -19
- package/templates/src/components/layout/Header.test.tsx +17 -1
- package/templates/src/components/layout/Header.tsx +11 -0
- package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +3 -3
- 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 +3 -3
- package/templates/src/components/shared/index.ts +1 -0
- package/templates/src/contexts/performanceContext.tsx +3 -3
- package/templates/src/contexts/queryContext.tsx +9 -8
- package/templates/src/contexts/supabaseContext.test.tsx +59 -0
- package/templates/src/contexts/supabaseContext.tsx +87 -0
- package/templates/src/hooks/index.ts +17 -0
- 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/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/env.ts +36 -14
- package/templates/src/lib/index.ts +4 -2
- package/templates/src/lib/routes.ts +1 -0
- package/templates/src/lib/sentry.ts +13 -10
- package/templates/src/lib/supabase/client.ts +58 -0
- package/templates/src/lib/supabase/index.ts +5 -0
- package/templates/src/main.ts +227 -0
- package/templates/src/main.tsx +32 -42
- 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/preload.ts +26 -0
- package/templates/src/stores/preferencesStore.ts +2 -1
- package/templates/src/test/clerkMock.tsx +49 -9
- package/templates/src/test/fetchMock.ts +58 -0
- package/templates/src/test/index.ts +49 -3
- package/templates/src/test/mocks.ts +128 -1
- package/templates/src/test/providers.tsx +7 -4
- package/templates/src/test/supabaseMock.ts +112 -0
- package/templates/src/test-setup.ts +26 -0
- package/templates/src/types/database.ts +46 -0
- package/templates/src/types/global.d.ts +28 -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/vite.main.config.mjs +20 -0
- package/templates/vite.preload.config.mjs +17 -0
- package/templates/vite.renderer.config.mjs +52 -0
|
@@ -30,7 +30,180 @@ import { screen, renderHook, act, waitFor, fireEvent } from '@testing-library/re
|
|
|
30
30
|
import userEvent from '@testing-library/user-event';
|
|
31
31
|
|
|
32
32
|
// Custom utilities from @/test
|
|
33
|
-
import { render,
|
|
33
|
+
import { render, server, mockMatchMedia, silenceConsoleError } from '@/test';
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Mock Infrastructure
|
|
37
|
+
|
|
38
|
+
The codebase uses a unified mocking approach with shared constants and reusable utilities.
|
|
39
|
+
|
|
40
|
+
### Directory Structure
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
src/test/ # Test utilities
|
|
44
|
+
├── index.ts # Public API - import from '@/test'
|
|
45
|
+
├── clerkMock.tsx # Clerk auth mocks
|
|
46
|
+
├── supabaseMock.ts # Supabase mocks
|
|
47
|
+
├── mocks.ts # Browser API mocks
|
|
48
|
+
└── providers.tsx # Test render wrapper
|
|
49
|
+
|
|
50
|
+
src/mocks/ # MSW infrastructure
|
|
51
|
+
├── constants.ts # Shared test values (MOCK_USER, etc.)
|
|
52
|
+
├── fixtures/ # Test data factories
|
|
53
|
+
│ ├── profiles.ts # createProfile(), mockProfiles
|
|
54
|
+
│ └── todos.ts # createTodo(), mockTodos
|
|
55
|
+
├── handlers/ # MSW request handlers
|
|
56
|
+
│ ├── supabase.ts # Supabase API mocks
|
|
57
|
+
│ └── todos.ts # JSONPlaceholder mocks
|
|
58
|
+
└── node.ts # MSW server setup
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Shared Constants
|
|
62
|
+
|
|
63
|
+
All mocks use shared constants from `@/mocks/constants.ts` to ensure consistency:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { MOCK_USER, MOCK_SESSION_ID } from '@/test';
|
|
67
|
+
|
|
68
|
+
// MOCK_USER.id = 'user_123'
|
|
69
|
+
// MOCK_USER.email = 'test@example.com'
|
|
70
|
+
// MOCK_USER.fullName = 'Test User'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Clerk Mocks
|
|
74
|
+
|
|
75
|
+
Control authentication state in tests:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { setMockClerkSignedIn, setMockClerkLoaded, resetClerkMocks } from '@/test';
|
|
79
|
+
|
|
80
|
+
beforeEach(() => resetClerkMocks());
|
|
81
|
+
|
|
82
|
+
it('shows sign-in when not authenticated', () => {
|
|
83
|
+
setMockClerkSignedIn(false);
|
|
84
|
+
render(<ProtectedRoute />);
|
|
85
|
+
expect(screen.getByTestId('sign-in-button')).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Supabase Mocks
|
|
90
|
+
|
|
91
|
+
Control Supabase query responses:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { setMockSupabaseData, setMockSupabaseError, createProfile, resetSupabaseMocks } from '@/test';
|
|
95
|
+
|
|
96
|
+
beforeEach(() => resetSupabaseMocks());
|
|
97
|
+
|
|
98
|
+
it('displays profile data', async () => {
|
|
99
|
+
setMockSupabaseData([createProfile({ full_name: 'John Doe' })]);
|
|
100
|
+
render(<ProfileCard />);
|
|
101
|
+
await waitFor(() => expect(screen.getByText('John Doe')).toBeInTheDocument());
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('handles database error', async () => {
|
|
105
|
+
setMockSupabaseError({ message: 'Connection failed', code: 'NETWORK_ERROR' });
|
|
106
|
+
render(<ProfileCard />);
|
|
107
|
+
await waitFor(() => expect(screen.getByText(/error/i)).toBeInTheDocument());
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Browser API Mocks
|
|
112
|
+
|
|
113
|
+
Reusable mocks for common browser APIs:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { mockMatchMedia, mockScrollTo, mockAnimationFrame, silenceConsoleError, silenceConsoleWarn } from '@/test';
|
|
117
|
+
|
|
118
|
+
// Media queries
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
window.matchMedia = mockMatchMedia(true); // matches
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Scroll behavior
|
|
124
|
+
it('scrolls to top', () => {
|
|
125
|
+
const scrollSpy = mockScrollTo();
|
|
126
|
+
triggerScroll();
|
|
127
|
+
expect(scrollSpy).toHaveBeenCalledWith(0, 0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Animation frames
|
|
131
|
+
let getCallback: () => FrameRequestCallback | null;
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
getCallback = mockAnimationFrame();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('updates on animation frame', () => {
|
|
137
|
+
const callback = getCallback();
|
|
138
|
+
act(() => callback?.(0));
|
|
139
|
+
// assert state change
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Silence console during error tests
|
|
143
|
+
it('handles error gracefully', () => {
|
|
144
|
+
const spy = silenceConsoleError();
|
|
145
|
+
triggerError();
|
|
146
|
+
expect(handleError).toHaveBeenCalled();
|
|
147
|
+
spy.mockRestore();
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Fetch Mocking
|
|
152
|
+
|
|
153
|
+
Use `vi.spyOn` for type-safe fetch mocking:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
vi.spyOn(global, 'fetch');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
afterEach(() => {
|
|
161
|
+
vi.restoreAllMocks();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('handles API response', async () => {
|
|
165
|
+
vi.mocked(global.fetch).mockResolvedValueOnce({
|
|
166
|
+
ok: true,
|
|
167
|
+
status: 200,
|
|
168
|
+
json: () => Promise.resolve({ data: 'test' }),
|
|
169
|
+
} as Response);
|
|
170
|
+
|
|
171
|
+
const result = await fetchData();
|
|
172
|
+
expect(result.data).toBe('test');
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### MSW (Mock Service Worker)
|
|
177
|
+
|
|
178
|
+
MSW handlers intercept HTTP requests. Override per-test:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { http, HttpResponse } from 'msw';
|
|
182
|
+
import { server } from '@/test';
|
|
183
|
+
|
|
184
|
+
it('handles API error', async () => {
|
|
185
|
+
server.use(http.get('/api/todos', () => new HttpResponse(null, { status: 500 })));
|
|
186
|
+
// Test error handling...
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
MSW handlers auto-reset after each test via `src/test-setup.ts`.
|
|
191
|
+
|
|
192
|
+
### Test Data Factories
|
|
193
|
+
|
|
194
|
+
Use factories to create test data with sensible defaults:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { createProfile, createTodo } from '@/test';
|
|
198
|
+
|
|
199
|
+
// Override specific fields
|
|
200
|
+
const profile = createProfile({ full_name: 'Custom Name' });
|
|
201
|
+
const todo = createTodo({ completed: true });
|
|
202
|
+
|
|
203
|
+
// Create multiple
|
|
204
|
+
import { createProfiles, createTodos } from '@/mocks/fixtures';
|
|
205
|
+
const profiles = createProfiles(5);
|
|
206
|
+
const todos = createTodos(10, { userId: 1 });
|
|
34
207
|
```
|
|
35
208
|
|
|
36
209
|
## Core Patterns
|
|
@@ -50,25 +223,6 @@ it.each([
|
|
|
50
223
|
// ❌ Avoid - repetitive individual tests
|
|
51
224
|
it('formats 0 bytes', () => expect(formatBytes(0)).toBe('0 Bytes'));
|
|
52
225
|
it('formats 1024 bytes', () => expect(formatBytes(1024)).toBe('1 KB'));
|
|
53
|
-
it('formats 1MB', () => expect(formatBytes(1024 * 1024)).toBe('1 MB'));
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Use Shared Mocks
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
import { mockMatchMedia } from '@/test';
|
|
60
|
-
|
|
61
|
-
describe('useMediaQuery', () => {
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
window.matchMedia = mockMatchMedia(false);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('detects desktop viewport', () => {
|
|
67
|
-
window.matchMedia = mockMatchMedia(true); // matches min-width query
|
|
68
|
-
const { result } = renderHook(() => useIsDesktop());
|
|
69
|
-
expect(result.current).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
226
|
```
|
|
73
227
|
|
|
74
228
|
### Component Testing
|
|
@@ -120,61 +274,6 @@ it('updates after async action', async () => {
|
|
|
120
274
|
});
|
|
121
275
|
```
|
|
122
276
|
|
|
123
|
-
### Mocking
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
import { mockMatchMedia } from '@/test';
|
|
127
|
-
|
|
128
|
-
// Mock modules at top of file
|
|
129
|
-
vi.mock('@/lib/storage', () => ({
|
|
130
|
-
setStorageItem: vi.fn(() => true),
|
|
131
|
-
}));
|
|
132
|
-
|
|
133
|
-
// Mock browser APIs using shared utilities
|
|
134
|
-
beforeEach(() => {
|
|
135
|
-
window.matchMedia = mockMatchMedia(false);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
afterEach(() => {
|
|
139
|
-
vi.restoreAllMocks();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Mock fetch for API tests
|
|
143
|
-
const mockFetch = vi.fn();
|
|
144
|
-
beforeEach(() => {
|
|
145
|
-
global.fetch = mockFetch;
|
|
146
|
-
});
|
|
147
|
-
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}) });
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### MSW (Mock Service Worker)
|
|
151
|
-
|
|
152
|
-
MSW handlers are organized in `src/mocks/`:
|
|
153
|
-
|
|
154
|
-
```
|
|
155
|
-
src/mocks/
|
|
156
|
-
├── handlers/
|
|
157
|
-
│ ├── index.ts # Combines all handlers
|
|
158
|
-
│ └── todos.ts # Example domain handlers
|
|
159
|
-
├── fixtures/
|
|
160
|
-
│ └── todos.ts # Response data
|
|
161
|
-
├── browser.ts # Browser worker setup
|
|
162
|
-
└── node.ts # Node server for tests
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
Override handlers per-test:
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
import { http, HttpResponse, server } from '@/test';
|
|
169
|
-
|
|
170
|
-
it('handles API error', async () => {
|
|
171
|
-
server.use(http.get('/api/todos', () => new HttpResponse(null, { status: 500 })));
|
|
172
|
-
// Test error handling...
|
|
173
|
-
});
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
MSW handlers auto-reset after each test via `src/test-setup.ts`.
|
|
177
|
-
|
|
178
277
|
### Store Testing (Zustand)
|
|
179
278
|
|
|
180
279
|
```typescript
|
|
@@ -192,6 +291,24 @@ describe('preferencesStore', () => {
|
|
|
192
291
|
});
|
|
193
292
|
```
|
|
194
293
|
|
|
294
|
+
## Mock Best Practices
|
|
295
|
+
|
|
296
|
+
### DO
|
|
297
|
+
|
|
298
|
+
- ✅ Use shared constants (`MOCK_USER`, etc.) for consistency
|
|
299
|
+
- ✅ Use `vi.spyOn()` for type-safe mocking
|
|
300
|
+
- ✅ Use `vi.mocked()` wrapper for TypeScript support
|
|
301
|
+
- ✅ Reset mocks in `beforeEach`/`afterEach`
|
|
302
|
+
- ✅ Restore mocks after silencing console
|
|
303
|
+
- ✅ Use factory functions for test data
|
|
304
|
+
|
|
305
|
+
### DON'T
|
|
306
|
+
|
|
307
|
+
- ❌ Hardcode user IDs or emails across files
|
|
308
|
+
- ❌ Use `global.fetch = mockFn` (use `vi.spyOn` instead)
|
|
309
|
+
- ❌ Create duplicate mock utilities in test files
|
|
310
|
+
- ❌ Forget to reset stateful mocks between tests
|
|
311
|
+
|
|
195
312
|
## Test Organization
|
|
196
313
|
|
|
197
314
|
### Structure Within Test Files
|
|
@@ -239,9 +356,10 @@ npm run test:ui # Visual UI
|
|
|
239
356
|
## Checklist for New Tests
|
|
240
357
|
|
|
241
358
|
- [ ] File follows `[name].test.ts(x)` naming
|
|
359
|
+
- [ ] Uses shared mocks from `@/test`
|
|
360
|
+
- [ ] Uses shared constants (`MOCK_USER`, etc.)
|
|
242
361
|
- [ ] Uses `it.each` for parameterized cases
|
|
243
|
-
- [ ] Mocks are
|
|
244
|
-
- [ ] No duplicate helper functions
|
|
362
|
+
- [ ] Mocks are reset in `beforeEach`/`afterEach`
|
|
245
363
|
- [ ] Tests behavior, not implementation
|
|
246
364
|
- [ ] Async tests use `await`/`waitFor` properly
|
|
247
365
|
- [ ] Coverage threshold maintained (80%)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { clerk, clerkSetup } from '@clerk/testing/playwright';
|
|
2
|
+
import { expect, test as setup } from '@playwright/test';
|
|
3
|
+
|
|
4
|
+
import { ASYNC_CONTENT_TIMEOUT, AUTH_STATE_FILE } from '../fixtures';
|
|
5
|
+
|
|
6
|
+
// Setup must be run serially for Clerk initialization
|
|
7
|
+
setup.describe.configure({ mode: 'serial' });
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configure Playwright with Clerk testing token.
|
|
11
|
+
* This obtains a Testing Token when the test suite starts.
|
|
12
|
+
*
|
|
13
|
+
* Required: CLERK_SECRET_KEY environment variable
|
|
14
|
+
*/
|
|
15
|
+
setup('global setup', async () => {
|
|
16
|
+
// Skip if CLERK_SECRET_KEY is not set
|
|
17
|
+
if (!process.env.CLERK_SECRET_KEY) {
|
|
18
|
+
setup.skip(true, 'CLERK_SECRET_KEY required for authenticated tests');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await clerkSetup();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Authenticate and save state to storage.
|
|
26
|
+
* This creates a reusable auth state for authenticated tests.
|
|
27
|
+
*
|
|
28
|
+
* Required environment variables:
|
|
29
|
+
* - E2E_CLERK_USER_USERNAME: Test user email
|
|
30
|
+
* - E2E_CLERK_USER_PASSWORD: Test user password
|
|
31
|
+
*/
|
|
32
|
+
setup('authenticate and save state', async ({ page }) => {
|
|
33
|
+
const username = process.env.E2E_CLERK_USER_USERNAME;
|
|
34
|
+
const password = process.env.E2E_CLERK_USER_PASSWORD;
|
|
35
|
+
|
|
36
|
+
// Skip auth setup if credentials are not provided
|
|
37
|
+
if (!username || !password) {
|
|
38
|
+
setup.skip(true, 'E2E_CLERK_USER_USERNAME and E2E_CLERK_USER_PASSWORD required for authenticated tests');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Navigate to app and sign in
|
|
43
|
+
await page.goto('/');
|
|
44
|
+
|
|
45
|
+
await clerk.signIn({
|
|
46
|
+
page,
|
|
47
|
+
signInParams: {
|
|
48
|
+
strategy: 'password',
|
|
49
|
+
identifier: username,
|
|
50
|
+
password: password,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Verify authentication succeeded by checking for authenticated UI
|
|
55
|
+
await page.goto('/profile');
|
|
56
|
+
await expect(page.getByText('Your Profile')).toBeVisible({ timeout: ASYNC_CONTENT_TIMEOUT });
|
|
57
|
+
|
|
58
|
+
// Save authentication state for reuse in other tests
|
|
59
|
+
await page.context().storageState({ path: AUTH_STATE_FILE });
|
|
60
|
+
});
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { Page } from '@playwright/test';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
// ESM-compatible path resolution
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
/** Path to Clerk auth state file for authenticated tests */
|
|
9
|
+
export const AUTH_STATE_FILE = join(__dirname, '../.clerk/user.json');
|
|
10
|
+
|
|
11
|
+
/** Default timeout for waiting on profile/async content to load */
|
|
12
|
+
export const ASYNC_CONTENT_TIMEOUT = 10000;
|
|
2
13
|
|
|
3
14
|
/**
|
|
4
15
|
* Navigate to page with clean state (clears localStorage).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
import { ASYNC_CONTENT_TIMEOUT } from '../fixtures';
|
|
4
|
+
|
|
5
|
+
// Check if auth credentials are configured
|
|
6
|
+
const hasAuthCredentials = !!(
|
|
7
|
+
process.env.CLERK_SECRET_KEY &&
|
|
8
|
+
process.env.E2E_CLERK_USER_USERNAME &&
|
|
9
|
+
process.env.E2E_CLERK_USER_PASSWORD
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Authenticated Profile Page E2E Tests
|
|
14
|
+
*
|
|
15
|
+
* These tests run with an authenticated user (from auth.setup.ts).
|
|
16
|
+
* They test the full profile CRUD flow with Supabase integration.
|
|
17
|
+
*
|
|
18
|
+
* Required environment variables:
|
|
19
|
+
* - E2E_CLERK_USER_USERNAME: Test user email
|
|
20
|
+
* - E2E_CLERK_USER_PASSWORD: Test user password
|
|
21
|
+
* - CLERK_SECRET_KEY: Clerk secret key for testing
|
|
22
|
+
*
|
|
23
|
+
* @see https://clerk.com/docs/testing/playwright
|
|
24
|
+
*/
|
|
25
|
+
test.describe('Authenticated Profile Tests', () => {
|
|
26
|
+
// Skip all tests if auth credentials aren't configured
|
|
27
|
+
test.skip(
|
|
28
|
+
!hasAuthCredentials,
|
|
29
|
+
'Auth credentials required (CLERK_SECRET_KEY, E2E_CLERK_USER_USERNAME, E2E_CLERK_USER_PASSWORD)',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
test.beforeEach(async ({ page }) => {
|
|
33
|
+
await page.goto('/profile');
|
|
34
|
+
// Wait for profile content to load (semantic selector instead of CSS class)
|
|
35
|
+
await expect(page.getByText('Full Name')).toBeVisible({ timeout: ASYNC_CONTENT_TIMEOUT });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('displays profile card with user info', async ({ page }) => {
|
|
39
|
+
await expect(page.getByText('Your Profile')).toBeVisible();
|
|
40
|
+
await expect(page.getByText('Manage your profile information stored in Supabase')).toBeVisible();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('shows user email in profile', async ({ page }) => {
|
|
44
|
+
const email = process.env.E2E_CLERK_USER_USERNAME;
|
|
45
|
+
if (email) {
|
|
46
|
+
await expect(page.getByText(email)).toBeVisible();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('can enter edit mode for name', async ({ page }) => {
|
|
51
|
+
await page.getByRole('button', { name: /edit/i }).click();
|
|
52
|
+
|
|
53
|
+
// Input field should appear
|
|
54
|
+
await expect(page.getByRole('textbox')).toBeVisible();
|
|
55
|
+
|
|
56
|
+
// Save and Cancel buttons should be visible
|
|
57
|
+
await expect(page.getByRole('button', { name: /save/i })).toBeVisible();
|
|
58
|
+
await expect(page.getByRole('button', { name: /cancel/i })).toBeVisible();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('can cancel edit without saving', async ({ page }) => {
|
|
62
|
+
// Enter edit mode
|
|
63
|
+
await page.getByRole('button', { name: /edit/i }).click();
|
|
64
|
+
|
|
65
|
+
// Type a new name
|
|
66
|
+
const input = page.getByRole('textbox');
|
|
67
|
+
await input.clear();
|
|
68
|
+
await input.fill('Temporary Name');
|
|
69
|
+
|
|
70
|
+
// Cancel
|
|
71
|
+
await page.getByRole('button', { name: /cancel/i }).click();
|
|
72
|
+
|
|
73
|
+
// Should exit edit mode (input gone, edit button back)
|
|
74
|
+
await expect(input).not.toBeVisible();
|
|
75
|
+
await expect(page.getByRole('button', { name: /edit/i })).toBeVisible();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('can update profile name', async ({ page }) => {
|
|
79
|
+
// Enter edit mode
|
|
80
|
+
await page.getByRole('button', { name: /edit/i }).click();
|
|
81
|
+
|
|
82
|
+
// Generate unique name with timestamp
|
|
83
|
+
const newName = `E2E Test User ${Date.now()}`;
|
|
84
|
+
|
|
85
|
+
// Update name
|
|
86
|
+
const input = page.getByRole('textbox');
|
|
87
|
+
await input.clear();
|
|
88
|
+
await input.fill(newName);
|
|
89
|
+
|
|
90
|
+
// Save
|
|
91
|
+
await page.getByRole('button', { name: /save/i }).click();
|
|
92
|
+
|
|
93
|
+
// Wait for save to complete (exit edit mode)
|
|
94
|
+
await expect(page.getByRole('button', { name: /edit/i })).toBeVisible({ timeout: ASYNC_CONTENT_TIMEOUT });
|
|
95
|
+
|
|
96
|
+
// Verify name is displayed
|
|
97
|
+
await expect(page.getByText(newName)).toBeVisible();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('Full Name label is present', async ({ page }) => {
|
|
101
|
+
await expect(page.getByText('Full Name')).toBeVisible();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Profile Page E2E Tests
|
|
5
|
+
*
|
|
6
|
+
* Note: These tests require a valid Clerk configuration to run.
|
|
7
|
+
* Without VITE_CLERK_PUBLISHABLE_KEY, the app may show an error page.
|
|
8
|
+
*
|
|
9
|
+
* For authenticated testing, see @clerk/testing package documentation.
|
|
10
|
+
* @see https://clerk.com/docs/testing/playwright
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
test.describe('Profile Page', () => {
|
|
14
|
+
test.beforeEach(async ({ page }) => {
|
|
15
|
+
// Check if the app loads (Clerk needs to be configured)
|
|
16
|
+
await page.goto('/');
|
|
17
|
+
|
|
18
|
+
// Wait for app to be ready (header should be visible if Clerk is configured)
|
|
19
|
+
const header = page.getByRole('banner');
|
|
20
|
+
const isAppReady = await header.isVisible({ timeout: 5000 }).catch(() => false);
|
|
21
|
+
|
|
22
|
+
if (!isAppReady) {
|
|
23
|
+
test.skip(true, 'App not ready - Clerk may not be configured');
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('protected route blocks unauthenticated access', async ({ page }) => {
|
|
28
|
+
await page.goto('/profile');
|
|
29
|
+
|
|
30
|
+
// Wait for Clerk to process the route
|
|
31
|
+
await page.waitForTimeout(3000);
|
|
32
|
+
|
|
33
|
+
// Profile content should NOT be visible without authentication
|
|
34
|
+
const profileCard = page.getByText('Your Profile');
|
|
35
|
+
const isVisible = await profileCard.isVisible().catch(() => false);
|
|
36
|
+
|
|
37
|
+
// Should either redirect or show sign-in UI, but NOT show profile
|
|
38
|
+
expect(isVisible).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('profile link only visible when authenticated', async ({ page }) => {
|
|
42
|
+
await page.goto('/');
|
|
43
|
+
|
|
44
|
+
// Wait for Clerk to initialize
|
|
45
|
+
await page.waitForTimeout(2000);
|
|
46
|
+
|
|
47
|
+
// Check for Profile link/button in header
|
|
48
|
+
// This is wrapped in <SignedIn>, so it should only appear for authenticated users
|
|
49
|
+
const profileButton = page.getByRole('button', { name: /profile/i });
|
|
50
|
+
const profileLink = page.getByRole('link', { name: /profile/i });
|
|
51
|
+
|
|
52
|
+
const buttonVisible = await profileButton.isVisible().catch(() => false);
|
|
53
|
+
const linkVisible = await profileLink.isVisible().catch(() => false);
|
|
54
|
+
|
|
55
|
+
// Without authentication, neither should be visible
|
|
56
|
+
// (unless user happens to be authenticated, which is also valid)
|
|
57
|
+
expect(buttonVisible || linkVisible).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @see profile.auth.spec.ts for authenticated profile tests (edit, update, etc.)
|
|
63
|
+
* @see https://clerk.com/docs/testing/playwright
|
|
64
|
+
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
test.describe('Registration Form', () => {
|
|
4
|
+
test.beforeEach(async ({ page }) => {
|
|
5
|
+
await page.goto('/');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test('displays registration form with all fields', async ({ page }) => {
|
|
9
|
+
await expect(page.getByLabel(/username/i)).toBeVisible();
|
|
10
|
+
await expect(page.getByLabel(/email/i)).toBeVisible();
|
|
11
|
+
await expect(page.getByLabel('Password', { exact: true })).toBeVisible();
|
|
12
|
+
await expect(page.getByLabel(/confirm password/i)).toBeVisible();
|
|
13
|
+
await expect(page.getByRole('button', { name: /create account/i })).toBeVisible();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('shows validation errors for empty submission', async ({ page }) => {
|
|
17
|
+
await page.getByRole('button', { name: /create account/i }).click();
|
|
18
|
+
|
|
19
|
+
await expect(page.getByText(/username must be at least/i)).toBeVisible();
|
|
20
|
+
await expect(page.getByText(/valid email address/i)).toBeVisible();
|
|
21
|
+
await expect(page.getByText(/password must be at least/i)).toBeVisible();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('validates password requirements', async ({ page }) => {
|
|
25
|
+
await page.getByLabel('Password', { exact: true }).fill('weak');
|
|
26
|
+
await page.getByRole('button', { name: /create account/i }).click();
|
|
27
|
+
|
|
28
|
+
await expect(page.getByText(/password must be at least 8 characters/i)).toBeVisible();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('validates password confirmation match', async ({ page }) => {
|
|
32
|
+
await page.getByLabel('Password', { exact: true }).fill('StrongPass1');
|
|
33
|
+
await page.getByLabel(/confirm password/i).fill('DifferentPass1');
|
|
34
|
+
await page.getByRole('button', { name: /create account/i }).click();
|
|
35
|
+
|
|
36
|
+
await expect(page.getByText(/passwords don't match/i)).toBeVisible();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @type {import('@electron-forge/shared-types').ForgeConfig} */
|
|
4
|
+
const config = {
|
|
5
|
+
packagerConfig: {
|
|
6
|
+
name: 'ReactSPAScaffold',
|
|
7
|
+
appBundleId: 'com.example.react-spa-scaffold',
|
|
8
|
+
// No code signing for internal/dev use
|
|
9
|
+
osxSign: undefined,
|
|
10
|
+
osxNotarize: undefined,
|
|
11
|
+
},
|
|
12
|
+
rebuildConfig: {},
|
|
13
|
+
makers: [
|
|
14
|
+
{
|
|
15
|
+
name: '@electron-forge/maker-zip',
|
|
16
|
+
platforms: ['darwin'],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: '@electron-forge/maker-dmg',
|
|
20
|
+
config: {
|
|
21
|
+
format: 'ULFO',
|
|
22
|
+
name: 'ReactSPAScaffold',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
plugins: [
|
|
27
|
+
{
|
|
28
|
+
name: '@electron-forge/plugin-vite',
|
|
29
|
+
config: {
|
|
30
|
+
build: [
|
|
31
|
+
{
|
|
32
|
+
entry: 'src/main.ts',
|
|
33
|
+
config: 'vite.main.config.mjs',
|
|
34
|
+
target: 'main',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
entry: 'src/preload.ts',
|
|
38
|
+
config: 'vite.preload.config.mjs',
|
|
39
|
+
target: 'preload',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
renderer: [
|
|
43
|
+
{
|
|
44
|
+
name: 'main_window',
|
|
45
|
+
config: 'vite.renderer.config.mjs',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default config;
|
package/templates/gitignore
CHANGED
|
@@ -23,10 +23,15 @@ coverage/
|
|
|
23
23
|
playwright-report/
|
|
24
24
|
test-results/
|
|
25
25
|
playwright/.cache/
|
|
26
|
+
e2e/.clerk/*
|
|
27
|
+
!e2e/.clerk/.gitkeep
|
|
26
28
|
|
|
27
29
|
# i18n compiled catalogs (generated by Vite plugin during build)
|
|
28
30
|
src/locales/*.mjs
|
|
29
31
|
|
|
32
|
+
# Supabase
|
|
33
|
+
supabase/.temp/
|
|
34
|
+
|
|
30
35
|
# Misc
|
|
31
36
|
tmp
|
|
32
37
|
*.log
|