@react-spa-scaffold/mcp 2.1.0 → 2.2.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 (101) hide show
  1. package/README.md +2 -1
  2. package/dist/constants.d.ts +1 -0
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +1 -0
  5. package/dist/constants.js.map +1 -1
  6. package/dist/features/definitions/auth.d.ts +3 -0
  7. package/dist/features/definitions/auth.d.ts.map +1 -0
  8. package/dist/features/definitions/auth.js +17 -0
  9. package/dist/features/definitions/auth.js.map +1 -0
  10. package/dist/features/definitions/core.d.ts.map +1 -1
  11. package/dist/features/definitions/core.js +16 -1
  12. package/dist/features/definitions/core.js.map +1 -1
  13. package/dist/features/definitions/forms.d.ts.map +1 -1
  14. package/dist/features/definitions/forms.js +4 -0
  15. package/dist/features/definitions/forms.js.map +1 -1
  16. package/dist/features/definitions/index.d.ts +1 -0
  17. package/dist/features/definitions/index.d.ts.map +1 -1
  18. package/dist/features/definitions/index.js +1 -0
  19. package/dist/features/definitions/index.js.map +1 -1
  20. package/dist/features/definitions/mobile.d.ts.map +1 -1
  21. package/dist/features/definitions/mobile.js +11 -2
  22. package/dist/features/definitions/mobile.js.map +1 -1
  23. package/dist/features/definitions/observability.js +1 -1
  24. package/dist/features/definitions/observability.js.map +1 -1
  25. package/dist/features/definitions/routing.d.ts.map +1 -1
  26. package/dist/features/definitions/routing.js +2 -1
  27. package/dist/features/definitions/routing.js.map +1 -1
  28. package/dist/features/definitions/state.d.ts.map +1 -1
  29. package/dist/features/definitions/state.js +9 -2
  30. package/dist/features/definitions/state.js.map +1 -1
  31. package/dist/features/definitions/testing.d.ts.map +1 -1
  32. package/dist/features/definitions/testing.js +4 -2
  33. package/dist/features/definitions/testing.js.map +1 -1
  34. package/dist/features/registry.d.ts.map +1 -1
  35. package/dist/features/registry.js +2 -1
  36. package/dist/features/registry.js.map +1 -1
  37. package/dist/features/types.test.js +4 -2
  38. package/dist/features/types.test.js.map +1 -1
  39. package/dist/tools/get-scaffold.d.ts.map +1 -1
  40. package/dist/tools/get-scaffold.js +6 -3
  41. package/dist/tools/get-scaffold.js.map +1 -1
  42. package/dist/tools/get-scaffold.test.js +5 -2
  43. package/dist/tools/get-scaffold.test.js.map +1 -1
  44. package/dist/utils/scaffold/compute.d.ts.map +1 -1
  45. package/dist/utils/scaffold/compute.js +3 -1
  46. package/dist/utils/scaffold/compute.js.map +1 -1
  47. package/dist/utils/scaffold/generators.d.ts.map +1 -1
  48. package/dist/utils/scaffold/generators.js +7 -0
  49. package/dist/utils/scaffold/generators.js.map +1 -1
  50. package/package.json +1 -1
  51. package/templates/.env.example +6 -0
  52. package/templates/.github/workflows/ci.yml +8 -3
  53. package/templates/CLAUDE.md +74 -1
  54. package/templates/docs/ARCHITECTURE.md +13 -12
  55. package/templates/docs/CODING_STANDARDS.md +65 -0
  56. package/templates/docs/E2E_TESTING.md +52 -7
  57. package/templates/e2e/fixtures/index.ts +13 -2
  58. package/templates/package.json +7 -3
  59. package/templates/playwright.config.ts +6 -1
  60. package/templates/src/components/layout/Header.tsx +2 -1
  61. package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +30 -0
  62. package/templates/src/components/shared/AccountButton/AccountButton.tsx +38 -0
  63. package/templates/src/components/shared/AccountButton/index.ts +1 -0
  64. package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.test.tsx +4 -4
  65. package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +55 -53
  66. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.test.tsx +43 -0
  67. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.tsx +35 -0
  68. package/templates/src/components/shared/ProtectedRoute/index.ts +1 -0
  69. package/templates/src/components/shared/index.ts +4 -2
  70. package/templates/src/contexts/clerkContext.tsx +45 -0
  71. package/templates/src/hooks/index.ts +23 -2
  72. package/templates/src/hooks/useCopyFeedback.test.ts +129 -0
  73. package/templates/src/hooks/useCopyFeedback.ts +41 -0
  74. package/templates/src/hooks/useDebouncedCallback.test.ts +164 -0
  75. package/templates/src/hooks/useDebouncedCallback.ts +47 -0
  76. package/templates/src/hooks/useDocumentTitle.test.ts +59 -0
  77. package/templates/src/hooks/useDocumentTitle.ts +31 -0
  78. package/templates/src/hooks/useIOSViewportReset.test.ts +58 -0
  79. package/templates/src/hooks/useIOSViewportReset.ts +18 -0
  80. package/templates/src/hooks/useKeyboardShortcut.test.ts +86 -0
  81. package/templates/src/hooks/useKeyboardShortcuts.ts +44 -0
  82. package/templates/src/hooks/useLocalStorage.test.ts +111 -0
  83. package/templates/src/hooks/useLocalStorage.ts +77 -0
  84. package/templates/src/hooks/useSyncedFormData.test.ts +75 -0
  85. package/templates/src/hooks/useSyncedFormData.ts +21 -0
  86. package/templates/src/hooks/useSyncedState.test.ts +119 -0
  87. package/templates/src/hooks/useSyncedState.ts +30 -0
  88. package/templates/src/index.css +1 -0
  89. package/templates/src/lib/constants.ts +10 -0
  90. package/templates/src/lib/createSelectors.test.ts +136 -0
  91. package/templates/src/lib/createSelectors.ts +31 -0
  92. package/templates/src/lib/index.ts +1 -0
  93. package/templates/src/lib/sentry.ts +55 -0
  94. package/templates/src/lib/storage.ts +6 -2
  95. package/templates/src/main.tsx +18 -8
  96. package/templates/src/stores/preferencesStore.ts +34 -9
  97. package/templates/src/test/clerkMock.tsx +97 -0
  98. package/templates/src/test/index.ts +3 -0
  99. package/templates/src/test/providers.tsx +7 -4
  100. package/templates/src/test-setup.ts +16 -2
  101. package/templates/vitest.config.ts +9 -1
@@ -1 +1 @@
1
- {"version":3,"file":"generators.js","sourceRoot":"","sources":["../../../src/utils/scaffold/generators.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"generators.js","sourceRoot":"","sources":["../../../src/utils/scaffold/generators.ts"],"names":[],"mappings":"AAAA,qCAAqC;AAErC;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,iEAAiE;AACjE,MAAM,UAAU,kBAAkB,CAAC,UAAuB;IACxD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,wCAAwC;IACxC,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,QAAQ,CAAC,IAAI,CAAC;;;EAGhB,CAAC,CAAC;IACF,CAAC;IAED,mCAAmC;IACnC,MAAM,OAAO,GAAa,CAAC,mCAAmC,EAAE,kCAAkC,CAAC,CAAC;IAEpG,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;IACrD,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,OAAO,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;IACjE,CAAC;IAED,0DAA0D;IAC1D,OAAO,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC;IACxE,OAAO,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IACzC,OAAO,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IAE1C,QAAQ,CAAC,IAAI,CAAC;;;EAGd,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;EAMlB,CAAC,CAAC;IAEF,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;AACtC,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAAC,UAAuB;IACnD,MAAM,YAAY,GAAa;QAC7B,gDAAgD;QAChD,8CAA8C;KAC/C,CAAC;IACF,MAAM,SAAS,GAAa;QAC1B,mDAAmD;QACnD,iDAAiD;KAClD,CAAC;IAEF,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,YAAY,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;QAClE,SAAS,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;IACpE,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/C,YAAY,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;QACrE,YAAY,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC;QACnE,SAAS,CAAC,IAAI,CAAC,uDAAuD,CAAC,CAAC;QACxE,SAAS,CAAC,IAAI,CAAC,+DAA+D,CAAC,CAAC;IAClF,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,YAAY,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC;QACtE,SAAS,CAAC,IAAI,CAAC,6EAA6E,CAAC,CAAC;IAChG,CAAC;IAED,2CAA2C;IAC3C,YAAY,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;IACnG,YAAY,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;IACxD,YAAY,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;IACzD,SAAS,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;IAClD,SAAS,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;IAChD,SAAS,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;IAElD,OAAO;;;;;;;;EAQP,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;EAWvB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;CAsBrB,CAAC;AACF,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;;;;;;;;;;CAWR,CAAC;AACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-spa-scaffold/mcp",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server for scaffolding projects based on react-spa-scaffold template",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,3 +19,9 @@ VITE_APP_URL=http://localhost:5173
19
19
  # - SENTRY_AUTH_TOKEN: API token for uploading source maps
20
20
  # - SENTRY_ORG: Sentry organization slug
21
21
  # - SENTRY_PROJECT: Sentry project slug
22
+
23
+ # ─────────────────────────────────────────────────────────────
24
+ # Clerk Authentication
25
+ # ─────────────────────────────────────────────────────────────
26
+ # Get your Publishable Key from: https://dashboard.clerk.com/~/api-keys
27
+ VITE_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY
@@ -84,11 +84,16 @@ jobs:
84
84
  fail-fast: false
85
85
  matrix:
86
86
  include:
87
- - type: functional
88
- project: functional
89
- command: npx playwright test --project=functional
87
+ - type: desktop
88
+ project: desktop
89
+ command: npx playwright test --project=desktop
90
90
  report-name: playwright-report
91
91
  upload-on: failure
92
+ - type: mobile
93
+ project: mobile
94
+ command: npx playwright test --project=mobile
95
+ report-name: playwright-report-mobile
96
+ upload-on: failure
92
97
  - type: performance
93
98
  project: performance
94
99
  command: PERF_TEST=true npx playwright test --project=performance
@@ -14,7 +14,9 @@ npm run format # Prettier format
14
14
  npm run test # Vitest once
15
15
  npm run test:watch # Vitest watch mode
16
16
  npm run test:coverage # Coverage (80% threshold)
17
- npm run e2e # Playwright functional E2E tests
17
+ npm run e2e # Playwright E2E (desktop)
18
+ npm run e2e:mobile # Playwright E2E (mobile)
19
+ npm run e2e:all # Playwright E2E (all viewports)
18
20
  npm run e2e:perf # Performance regression tests
19
21
  npm run i18n:extract # Extract translations to .po
20
22
  ```
@@ -50,6 +52,76 @@ e2e/ # Playwright tests
50
52
 
51
53
  See [docs/CODING_STANDARDS.md](docs/CODING_STANDARDS.md) and [docs/COMPONENT_GUIDELINES.md](docs/COMPONENT_GUIDELINES.md).
52
54
 
55
+ ## Custom Hooks
56
+
57
+ ### State & Storage
58
+
59
+ ```tsx
60
+ import { useLocalStorage } from '@/hooks';
61
+
62
+ // localStorage with tab sync and updater functions
63
+ const [value, setValue] = useLocalStorage('key', defaultValue);
64
+ setValue((prev) => newValue);
65
+ ```
66
+
67
+ ### Form State Sync
68
+
69
+ ```tsx
70
+ import { useSyncedFormData, useSyncedState } from '@/hooks';
71
+
72
+ // Sync form data when trigger changes (dialog open, ID changes)
73
+ const [formData, setFormData] = useSyncedFormData(sourceData, syncTrigger);
74
+
75
+ // Sync state but block when actively editing
76
+ const [localValue, setLocalValue] = useSyncedState(externalValue, isEditing);
77
+ ```
78
+
79
+ ### Utilities
80
+
81
+ ```tsx
82
+ import { useCopyFeedback, useDebouncedCallback, useKeyboardShortcut, useDocumentTitle } from '@/hooks';
83
+
84
+ // Copy feedback with auto-reset
85
+ const { isCopied, triggerCopied } = useCopyFeedback(2000);
86
+
87
+ // Debounced callbacks
88
+ const debouncedSearch = useDebouncedCallback(handleSearch, 300);
89
+
90
+ // Keyboard shortcuts
91
+ useKeyboardShortcut('mod+s', handleSave, { preventDefault: true });
92
+
93
+ // Dynamic page titles
94
+ useDocumentTitle('Dashboard');
95
+ ```
96
+
97
+ ### Mobile & iOS
98
+
99
+ ```tsx
100
+ import { useIOSViewportReset, useMobileContext, useTouchSizes } from '@/hooks';
101
+
102
+ // iOS Safari keyboard viewport fix
103
+ const handleBlur = useIOSViewportReset();
104
+ <input onBlur={handleBlur} />;
105
+
106
+ // Responsive breakpoints
107
+ const { isMobile, isTablet, isDesktop } = useMobileContext();
108
+
109
+ // Touch-aware sizes (44px on mobile)
110
+ const sizes = useTouchSizes();
111
+ <Button size={sizes.button}>Click</Button>;
112
+ ```
113
+
114
+ ## TIMING Constants
115
+
116
+ Use centralized timing constants for consistent UX:
117
+
118
+ ```tsx
119
+ import { TIMING } from '@/lib/constants';
120
+
121
+ // TIMING.DEBOUNCE_DELAY = 300ms
122
+ // TIMING.COPY_FEEDBACK_DURATION = 2000ms
123
+ ```
124
+
53
125
  ## UI Components (Shadcn/UI)
54
126
 
55
127
  This project uses **Shadcn/UI** with radix-nova style. Components live in `src/components/ui/`.
@@ -104,6 +176,7 @@ resolve-library-id → get-library-docs
104
176
  - `zod` - Schema validation
105
177
  - `date-fns` - Date formatting
106
178
  - `msw` - Mock service worker setup
179
+ - `@clerk/react-router` - Authentication patterns
107
180
 
108
181
  ### Decision Flow
109
182
 
@@ -12,6 +12,7 @@ High-level architecture and key decisions. For API details, see [API Reference](
12
12
  | Styling | Tailwind CSS 4 | No runtime cost, scales with team size |
13
13
  | State | Zustand + TanStack Query | Minimal boilerplate, separation of concerns |
14
14
  | Forms | React Hook Form + Zod | Minimal re-renders, type-safe validation |
15
+ | Authentication | Clerk | Modal-based auth, shadcn theme integration |
15
16
  | i18n | Lingui | Smaller runtime, compile-time extraction |
16
17
  | Testing | Vitest + Playwright | Fast, Vite-native, true cross-browser |
17
18
  | Error Tracking | Sentry | Industry standard, lazy-loaded |
@@ -73,22 +74,21 @@ Providers wrap the app in this specific order:
73
74
  ```tsx
74
75
  <StrictMode>
75
76
  <QueryProvider>
76
- {' '}
77
77
  {/* TanStack Query - outermost for global cache */}
78
78
  <I18nProvider>
79
- {' '}
80
79
  {/* Lingui - translations available everywhere */}
81
80
  <BrowserRouter>
82
- {' '}
83
81
  {/* React Router - routing context */}
84
- <MobileProvider>
85
- {' '}
86
- {/* Viewport - depends on router for SSR */}
87
- <ErrorBoundary>
88
- <App />
89
- <Toaster />
90
- </ErrorBoundary>
91
- </MobileProvider>
82
+ <ClerkThemeProvider>
83
+ {/* Clerk - auth inside Router for @clerk/react-router */}
84
+ <MobileProvider>
85
+ {/* Viewport - depends on router for SSR */}
86
+ <ErrorBoundary>
87
+ <App />
88
+ <Toaster />
89
+ </ErrorBoundary>
90
+ </MobileProvider>
91
+ </ClerkThemeProvider>
92
92
  </BrowserRouter>
93
93
  </I18nProvider>
94
94
  </QueryProvider>
@@ -99,7 +99,8 @@ Providers wrap the app in this specific order:
99
99
 
100
100
  - QueryProvider outermost so cache persists across route changes
101
101
  - I18nProvider before Router so route components can use translations
102
- - MobileProvider inside Router for potential SSR viewport detection
102
+ - ClerkThemeProvider inside Router (required by @clerk/react-router declarative mode)
103
+ - MobileProvider inside Clerk for potential SSR viewport detection
103
104
  - ErrorBoundary innermost to catch errors in App without breaking providers
104
105
 
105
106
  ### 2. State Management Separation
@@ -19,6 +19,71 @@ interface User {
19
19
 
20
20
  See [Architecture Guide](./ARCHITECTURE.md#state-management) for when to use each solution.
21
21
 
22
+ ### Zustand Best Practices
23
+
24
+ **Auto-generated selectors**: All stores use `createSelectors` for cleaner access:
25
+
26
+ ```tsx
27
+ // Store definition
28
+ const useStoreBase = create<State>()(/* ... */);
29
+ export const useStore = createSelectors(useStoreBase);
30
+
31
+ // Component usage - auto-generated selectors
32
+ const count = useStore.use.count();
33
+ const increment = useStore.use.increment();
34
+ ```
35
+
36
+ **Use `useShallow` for multiple values**: Prevents unnecessary re-renders:
37
+
38
+ ```tsx
39
+ import { useShallow } from 'zustand/react/shallow';
40
+
41
+ // Group state values with useShallow
42
+ const { searchQuery, sortBy } = useStore(
43
+ useShallow((s) => ({
44
+ searchQuery: s.searchQuery,
45
+ sortBy: s.sortBy,
46
+ })),
47
+ );
48
+ ```
49
+
50
+ **Persist versioning**: Always include version and migrate for persisted stores:
51
+
52
+ ```tsx
53
+ persist(
54
+ (set, get) => ({
55
+ /* ... */
56
+ }),
57
+ {
58
+ name: 'store-key',
59
+ version: 1, // Increment on breaking changes
60
+ migrate: (persisted, version) => {
61
+ if (version === 0) {
62
+ return { ...persisted, newField: 'default' };
63
+ }
64
+ return persisted;
65
+ },
66
+ },
67
+ );
68
+ ```
69
+
70
+ **Middleware order**: Stack middlewares correctly:
71
+
72
+ ```tsx
73
+ // devtools → persist → subscribeWithSelector → store
74
+ create<State>()(
75
+ devtools(
76
+ persist(
77
+ subscribeWithSelector((set, get) => ({
78
+ /* ... */
79
+ })),
80
+ { name: 'key' },
81
+ ),
82
+ { name: 'StoreName', enabled: process.env.NODE_ENV === 'development' },
83
+ ),
84
+ );
85
+ ```
86
+
22
87
  ### Query Hooks
23
88
 
24
89
  Extract the fetcher function:
@@ -11,7 +11,7 @@
11
11
  ```
12
12
  e2e/
13
13
  ├── fixtures/
14
- │ └── index.ts # setupPage, clearAppState
14
+ │ └── index.ts # setupPage, setupCleanPage, test, expect
15
15
  ├── tests/ # Functional E2E tests
16
16
  │ ├── home.spec.ts # Page structure, accessibility
17
17
  │ ├── theme.spec.ts # Theme toggle, persistence
@@ -26,9 +26,7 @@ e2e/
26
26
 
27
27
  ```typescript
28
28
  import { expect, test } from '@playwright/test';
29
-
30
- // For tests that need state clearing
31
- import { setupPage } from '../fixtures';
29
+ import { setupPage, setupCleanPage } from '../fixtures';
32
30
  ```
33
31
 
34
32
  ## Core Patterns
@@ -107,13 +105,59 @@ await page.waitForTimeout(500);
107
105
  ## Running Tests
108
106
 
109
107
  ```bash
110
- npm run e2e # Run functional tests
111
- npm run e2e:ui # Functional tests with interactive UI
108
+ npm run e2e # Run desktop tests
109
+ npm run e2e:mobile # Run mobile tests (Pixel 5 emulation)
110
+ npm run e2e:all # Run both desktop and mobile
111
+ npm run e2e:ui # Interactive UI mode
112
112
  npm run e2e:perf # Run performance tests
113
113
  npm run e2e:perf:ui # Performance tests with interactive UI
114
- npm run e2e:all # Run all tests (functional + performance)
115
114
  ```
116
115
 
116
+ ## Mobile Testing
117
+
118
+ Tests run on both desktop (Chrome) and mobile (Pixel 5) viewports. Use the `isMobile` fixture for device-specific behavior.
119
+
120
+ ### Using isMobile Fixture
121
+
122
+ ```typescript
123
+ import { expect, test } from '@playwright/test';
124
+
125
+ test('theme toggle works on all devices', async ({ page, isMobile }) => {
126
+ await page.goto('/');
127
+
128
+ // Same test logic works on both platforms
129
+ await page.getByRole('button', { name: /dark mode/i }).click();
130
+ await expect(page.locator('html')).toHaveClass(/dark/);
131
+
132
+ // Add mobile-specific assertions if needed
133
+ if (isMobile) {
134
+ // Verify touch-friendly button size, etc.
135
+ }
136
+ });
137
+ ```
138
+
139
+ ### Skip Tests by Platform
140
+
141
+ ```typescript
142
+ test('hover tooltip shows', async ({ page, isMobile }) => {
143
+ test.skip(isMobile, 'Hover not available on touch devices');
144
+ // Desktop-only test
145
+ });
146
+
147
+ test('touch gesture works', async ({ page, isMobile }) => {
148
+ test.skip(!isMobile, 'Touch gesture only on mobile');
149
+ // Mobile-only test
150
+ });
151
+ ```
152
+
153
+ ### Common Patterns
154
+
155
+ | Pattern | Desktop | Mobile |
156
+ | -------------- | --------- | --------------- |
157
+ | Viewport width | 1280px | 393px (Pixel 5) |
158
+ | Touch events | Click | Tap |
159
+ | Hover states | Supported | Not applicable |
160
+
117
161
  ## Performance Testing
118
162
 
119
163
  Performance tests use [react-performance-tracking](https://github.com/mkaczkowski/react-performance-tracking) to measure:
@@ -129,3 +173,4 @@ Performance tests use [react-performance-tracking](https://github.com/mkaczkowsk
129
173
  - [ ] No arbitrary timeouts
130
174
  - [ ] Tests behavior, not implementation
131
175
  - [ ] Uses `setupPage` when testing persistence
176
+ - [ ] Considers mobile viewport when relevant
@@ -1,10 +1,21 @@
1
1
  import type { Page } from '@playwright/test';
2
2
 
3
3
  /**
4
- * Navigate to page with clean state (clears localStorage)
4
+ * Navigate to page with clean state (clears localStorage).
5
5
  */
6
- export async function setupPage(page: Page, path = '/') {
6
+ export async function setupPage(page: Page, path = '/'): Promise<void> {
7
7
  await page.goto(path);
8
8
  await page.evaluate(() => localStorage.clear());
9
9
  await page.reload();
10
10
  }
11
+
12
+ /**
13
+ * Setup page with completely fresh state (cookies + localStorage).
14
+ * Use when tests need isolation from previous test state.
15
+ */
16
+ export async function setupCleanPage(page: Page): Promise<void> {
17
+ await page.context().clearCookies();
18
+ await page.goto('/');
19
+ await page.evaluate(() => localStorage.clear());
20
+ await page.reload();
21
+ }
@@ -39,11 +39,12 @@
39
39
  "test": "vitest run",
40
40
  "test:watch": "vitest",
41
41
  "test:coverage": "vitest run --coverage",
42
- "e2e": "playwright test --project=functional",
43
- "e2e:ui": "playwright test --project=functional --ui",
42
+ "e2e": "playwright test --project=desktop",
43
+ "e2e:mobile": "playwright test --project=mobile",
44
+ "e2e:all": "playwright test --project=desktop --project=mobile",
45
+ "e2e:ui": "playwright test --ui",
44
46
  "e2e:perf": "PERF_TEST=true playwright test --project=performance",
45
47
  "e2e:perf:ui": "PERF_TEST=true playwright test --project=performance --ui",
46
- "e2e:all": "PERF_TEST=true playwright test",
47
48
  "i18n:extract": "lingui extract",
48
49
  "prepare": "husky",
49
50
  "mcp:build": "npm run build -w @react-spa-scaffold/mcp",
@@ -55,6 +56,8 @@
55
56
  "release": "changeset publish"
56
57
  },
57
58
  "dependencies": {
59
+ "@clerk/react-router": "^2.3.7",
60
+ "@clerk/themes": "^2.4.46",
58
61
  "@fontsource-variable/inter": "^5.2.5",
59
62
  "@hookform/resolvers": "^5.0.1",
60
63
  "@lingui/core": "^5.7.0",
@@ -69,6 +72,7 @@
69
72
  "react": "^19.1.0",
70
73
  "react-dom": "^19.1.0",
71
74
  "react-hook-form": "^7.58.0",
75
+ "react-hotkeys-hook": "^5.2.1",
72
76
  "react-performance-tracking": "^1.2.1",
73
77
  "react-router": "^7.11.0",
74
78
  "sonner": "^2.0.7",
@@ -17,10 +17,15 @@ export default defineConfig({
17
17
  },
18
18
  projects: [
19
19
  {
20
- name: 'functional',
20
+ name: 'desktop',
21
21
  testDir: './e2e/tests',
22
22
  use: { ...devices['Desktop Chrome'] },
23
23
  },
24
+ {
25
+ name: 'mobile',
26
+ testDir: './e2e/tests',
27
+ use: { ...devices['Pixel 5'] },
28
+ },
24
29
  {
25
30
  name: 'performance',
26
31
  testDir: './e2e/performance',
@@ -1,6 +1,6 @@
1
1
  import { Trans } from '@lingui/react/macro';
2
2
 
3
- import { LanguageSwitcher, ThemeToggle } from '@/components/shared';
3
+ import { AccountButton, LanguageSwitcher, ThemeToggle } from '@/components/shared';
4
4
 
5
5
  export function Header() {
6
6
  return (
@@ -12,6 +12,7 @@ export function Header() {
12
12
  <div className="flex items-center gap-2">
13
13
  <LanguageSwitcher />
14
14
  <ThemeToggle />
15
+ <AccountButton />
15
16
  </div>
16
17
  </div>
17
18
  </header>
@@ -0,0 +1,30 @@
1
+ import { screen } from '@testing-library/react';
2
+ import { describe, it, expect, beforeEach } from 'vitest';
3
+
4
+ import { render, setMockSignedIn, setMockLoaded, resetClerkMocks } from '@/test';
5
+
6
+ import { AccountButton } from './AccountButton';
7
+
8
+ describe('AccountButton', () => {
9
+ beforeEach(() => {
10
+ resetClerkMocks();
11
+ });
12
+
13
+ it('shows skeleton when not loaded', () => {
14
+ setMockLoaded(false);
15
+ const { container } = render(<AccountButton />);
16
+ expect(container.querySelector('.rounded-full')).toBeInTheDocument();
17
+ });
18
+
19
+ it('shows user button when signed in', () => {
20
+ render(<AccountButton />);
21
+ expect(screen.getByTestId('user-button')).toBeInTheDocument();
22
+ });
23
+
24
+ it('shows sign in button when signed out', () => {
25
+ setMockSignedIn(false);
26
+ render(<AccountButton />);
27
+ expect(screen.getByTestId('sign-in-button')).toBeInTheDocument();
28
+ expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
29
+ });
30
+ });
@@ -0,0 +1,38 @@
1
+ import { SignedIn, SignedOut, SignInButton, useAuth, UserButton } from '@clerk/react-router';
2
+ import { useLingui } from '@lingui/react/macro';
3
+ import { User } from 'lucide-react';
4
+
5
+ import { Button } from '@/components/ui/button';
6
+ import { Skeleton } from '@/components/ui/skeleton';
7
+ import { useTouchSizes } from '@/hooks';
8
+
9
+ export function AccountButton() {
10
+ const { t } = useLingui();
11
+ const { isLoaded } = useAuth();
12
+ const sizes = useTouchSizes();
13
+
14
+ // Show skeleton while Clerk loads to prevent UI flash
15
+ if (!isLoaded) {
16
+ return <Skeleton className="size-9 rounded-full" />;
17
+ }
18
+
19
+ return (
20
+ <>
21
+ <SignedOut>
22
+ {/* eslint-disable-next-line lingui/no-unlocalized-strings */}
23
+ <SignInButton mode="modal">
24
+ <Button
25
+ variant="ghost"
26
+ size={sizes.iconButtonLg}
27
+ aria-label={t({ message: 'Sign in', comment: 'Sign in button aria label' })}
28
+ >
29
+ <User className="size-5" />
30
+ </Button>
31
+ </SignInButton>
32
+ </SignedOut>
33
+ <SignedIn>
34
+ <UserButton />
35
+ </SignedIn>
36
+ </>
37
+ );
38
+ }
@@ -0,0 +1 @@
1
+ export { AccountButton } from './AccountButton';
@@ -52,14 +52,14 @@ describe('ErrorBoundary', () => {
52
52
  expect(screen.getByRole('button', { name: /try again/i })).toBeInTheDocument();
53
53
  });
54
54
 
55
- it('shows Refresh Page button', () => {
55
+ it('shows Reload Page button', () => {
56
56
  render(
57
57
  <ErrorBoundary>
58
58
  <ThrowingComponent />
59
59
  </ErrorBoundary>,
60
60
  );
61
61
 
62
- expect(screen.getByRole('button', { name: /refresh page/i })).toBeInTheDocument();
62
+ expect(screen.getByRole('button', { name: /reload page/i })).toBeInTheDocument();
63
63
  });
64
64
 
65
65
  it('renders custom fallback when provided', () => {
@@ -103,7 +103,7 @@ describe('ErrorBoundary', () => {
103
103
  expect(onReset).toHaveBeenCalledTimes(1);
104
104
  });
105
105
 
106
- it('reloads page when Refresh Page is clicked', () => {
106
+ it('reloads page when Reload Page is clicked', () => {
107
107
  const reloadMock = vi.fn();
108
108
  const originalLocation = window.location;
109
109
 
@@ -119,7 +119,7 @@ describe('ErrorBoundary', () => {
119
119
  </ErrorBoundary>,
120
120
  );
121
121
 
122
- fireEvent.click(screen.getByRole('button', { name: /refresh page/i }));
122
+ fireEvent.click(screen.getByRole('button', { name: /reload page/i }));
123
123
 
124
124
  expect(reloadMock).toHaveBeenCalledTimes(1);
125
125