@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.
Files changed (138) hide show
  1. package/dist/constants.d.ts +4 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +4 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/features/definitions/api.d.ts.map +1 -1
  6. package/dist/features/definitions/api.js +2 -1
  7. package/dist/features/definitions/api.js.map +1 -1
  8. package/dist/features/definitions/database.d.ts +3 -0
  9. package/dist/features/definitions/database.d.ts.map +1 -0
  10. package/dist/features/definitions/database.js +45 -0
  11. package/dist/features/definitions/database.js.map +1 -0
  12. package/dist/features/definitions/deployment.d.ts +3 -0
  13. package/dist/features/definitions/deployment.d.ts.map +1 -0
  14. package/dist/features/definitions/deployment.js +14 -0
  15. package/dist/features/definitions/deployment.js.map +1 -0
  16. package/dist/features/definitions/electron.d.ts +3 -0
  17. package/dist/features/definitions/electron.d.ts.map +1 -0
  18. package/dist/features/definitions/electron.js +23 -0
  19. package/dist/features/definitions/electron.js.map +1 -0
  20. package/dist/features/definitions/index.d.ts +3 -0
  21. package/dist/features/definitions/index.d.ts.map +1 -1
  22. package/dist/features/definitions/index.js +3 -0
  23. package/dist/features/definitions/index.js.map +1 -1
  24. package/dist/features/registry.d.ts.map +1 -1
  25. package/dist/features/registry.js +4 -1
  26. package/dist/features/registry.js.map +1 -1
  27. package/dist/features/types.d.ts +1 -0
  28. package/dist/features/types.d.ts.map +1 -1
  29. package/dist/features/types.test.js +5 -2
  30. package/dist/features/types.test.js.map +1 -1
  31. package/dist/resources/docs.d.ts.map +1 -1
  32. package/dist/resources/docs.js +5 -0
  33. package/dist/resources/docs.js.map +1 -1
  34. package/dist/tools/add-features.js +1 -1
  35. package/dist/tools/add-features.js.map +1 -1
  36. package/dist/tools/get-features.test.js +7 -0
  37. package/dist/tools/get-features.test.js.map +1 -1
  38. package/dist/tools/get-scaffold.d.ts +1 -0
  39. package/dist/tools/get-scaffold.d.ts.map +1 -1
  40. package/dist/tools/get-scaffold.js +4 -1
  41. package/dist/tools/get-scaffold.js.map +1 -1
  42. package/dist/tools/get-scaffold.test.js +50 -0
  43. package/dist/tools/get-scaffold.test.js.map +1 -1
  44. package/dist/utils/docs.d.ts.map +1 -1
  45. package/dist/utils/docs.js +2 -0
  46. package/dist/utils/docs.js.map +1 -1
  47. package/dist/utils/scaffold/claude-md/index.d.ts.map +1 -1
  48. package/dist/utils/scaffold/claude-md/index.js +4 -1
  49. package/dist/utils/scaffold/claude-md/index.js.map +1 -1
  50. package/dist/utils/scaffold/claude-md/sections.d.ts +3 -0
  51. package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
  52. package/dist/utils/scaffold/claude-md/sections.js +174 -2
  53. package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
  54. package/dist/utils/scaffold/compute.d.ts.map +1 -1
  55. package/dist/utils/scaffold/compute.js +4 -2
  56. package/dist/utils/scaffold/compute.js.map +1 -1
  57. package/dist/utils/scaffold/generators.d.ts +7 -2
  58. package/dist/utils/scaffold/generators.d.ts.map +1 -1
  59. package/dist/utils/scaffold/generators.js +100 -22
  60. package/dist/utils/scaffold/generators.js.map +1 -1
  61. package/package.json +1 -1
  62. package/templates/.env.example +40 -12
  63. package/templates/.github/workflows/ci.yml +49 -2
  64. package/templates/.github/workflows/deploy.yml +46 -0
  65. package/templates/CLAUDE.md +180 -1
  66. package/templates/docs/AUTHENTICATION.md +325 -0
  67. package/templates/docs/DEPLOYMENT.md +296 -0
  68. package/templates/docs/E2E_TESTING.md +81 -4
  69. package/templates/docs/SUPABASE_INTEGRATION.md +310 -0
  70. package/templates/docs/TESTING.md +195 -77
  71. package/templates/e2e/auth/auth.setup.ts +60 -0
  72. package/templates/e2e/fixtures/index.ts +11 -0
  73. package/templates/e2e/tests/profile.auth.spec.ts +103 -0
  74. package/templates/e2e/tests/profile.spec.ts +64 -0
  75. package/templates/e2e/tests/register-form.spec.ts +38 -0
  76. package/templates/forge.config.js +53 -0
  77. package/templates/gitignore +5 -0
  78. package/templates/package.json +13 -1
  79. package/templates/playwright.config.ts +33 -3
  80. package/templates/src/App.tsx +32 -19
  81. package/templates/src/components/layout/Header.test.tsx +17 -1
  82. package/templates/src/components/layout/Header.tsx +11 -0
  83. package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +3 -3
  84. package/templates/src/components/shared/ProfileSync/ProfileSync.test.tsx +44 -0
  85. package/templates/src/components/shared/ProfileSync/ProfileSync.tsx +104 -0
  86. package/templates/src/components/shared/ProfileSync/index.ts +1 -0
  87. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.test.tsx +3 -3
  88. package/templates/src/components/shared/index.ts +1 -0
  89. package/templates/src/contexts/performanceContext.tsx +3 -3
  90. package/templates/src/contexts/queryContext.tsx +9 -8
  91. package/templates/src/contexts/supabaseContext.test.tsx +59 -0
  92. package/templates/src/contexts/supabaseContext.tsx +87 -0
  93. package/templates/src/hooks/index.ts +17 -0
  94. package/templates/src/hooks/supabase/index.ts +12 -0
  95. package/templates/src/hooks/supabase/useProfiles.test.tsx +207 -0
  96. package/templates/src/hooks/supabase/useProfiles.ts +213 -0
  97. package/templates/src/hooks/supabase/useSupabaseQuery.test.tsx +150 -0
  98. package/templates/src/hooks/supabase/useSupabaseQuery.ts +91 -0
  99. package/templates/src/lib/api.test.ts +30 -38
  100. package/templates/src/lib/api.ts +1 -7
  101. package/templates/src/lib/config.ts +54 -4
  102. package/templates/src/lib/env.ts +36 -14
  103. package/templates/src/lib/index.ts +4 -2
  104. package/templates/src/lib/routes.ts +1 -0
  105. package/templates/src/lib/sentry.ts +13 -10
  106. package/templates/src/lib/supabase/client.ts +58 -0
  107. package/templates/src/lib/supabase/index.ts +5 -0
  108. package/templates/src/main.ts +227 -0
  109. package/templates/src/main.tsx +32 -42
  110. package/templates/src/mocks/constants.ts +31 -0
  111. package/templates/src/mocks/fixtures/index.ts +3 -1
  112. package/templates/src/mocks/fixtures/profiles.ts +55 -0
  113. package/templates/src/mocks/fixtures/users.ts +91 -0
  114. package/templates/src/mocks/handlers/index.ts +2 -1
  115. package/templates/src/mocks/handlers/supabase.ts +64 -0
  116. package/templates/src/mocks/handlers/todos.ts +1 -1
  117. package/templates/src/mocks/index.ts +6 -0
  118. package/templates/src/pages/Profile.test.tsx +263 -0
  119. package/templates/src/pages/Profile.tsx +171 -0
  120. package/templates/src/pages/index.ts +1 -0
  121. package/templates/src/preload.ts +26 -0
  122. package/templates/src/stores/preferencesStore.ts +2 -1
  123. package/templates/src/test/clerkMock.tsx +49 -9
  124. package/templates/src/test/fetchMock.ts +58 -0
  125. package/templates/src/test/index.ts +49 -3
  126. package/templates/src/test/mocks.ts +128 -1
  127. package/templates/src/test/providers.tsx +7 -4
  128. package/templates/src/test/supabaseMock.ts +112 -0
  129. package/templates/src/test-setup.ts +26 -0
  130. package/templates/src/types/database.ts +46 -0
  131. package/templates/src/types/global.d.ts +28 -0
  132. package/templates/src/types/index.ts +1 -0
  133. package/templates/src/types/supabase.ts +167 -0
  134. package/templates/src/vite-env.d.ts +6 -0
  135. package/templates/supabase/migrations/20260104000000_create_profiles_table.sql +67 -0
  136. package/templates/vite.main.config.mjs +20 -0
  137. package/templates/vite.preload.config.mjs +17 -0
  138. 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, mockMatchMedia, createTestQueryClient, server } from '@/test';
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 cleared in `beforeEach`/`afterEach`
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;
@@ -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