@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.
- package/README.md +2 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -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/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 +1 -0
- package/dist/features/definitions/index.d.ts.map +1 -1
- package/dist/features/definitions/index.js +1 -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 +2 -1
- package/dist/features/registry.js.map +1 -1
- package/dist/features/types.test.js +4 -2
- package/dist/features/types.test.js.map +1 -1
- package/dist/tools/get-scaffold.d.ts.map +1 -1
- package/dist/tools/get-scaffold.js +6 -3
- package/dist/tools/get-scaffold.js.map +1 -1
- package/dist/tools/get-scaffold.test.js +5 -2
- package/dist/tools/get-scaffold.test.js.map +1 -1
- package/dist/utils/scaffold/compute.d.ts.map +1 -1
- package/dist/utils/scaffold/compute.js +3 -1
- package/dist/utils/scaffold/compute.js.map +1 -1
- package/dist/utils/scaffold/generators.d.ts.map +1 -1
- package/dist/utils/scaffold/generators.js +7 -0
- package/dist/utils/scaffold/generators.js.map +1 -1
- package/package.json +1 -1
- package/templates/.env.example +6 -0
- package/templates/.github/workflows/ci.yml +8 -3
- package/templates/CLAUDE.md +74 -1
- package/templates/docs/ARCHITECTURE.md +13 -12
- package/templates/docs/CODING_STANDARDS.md +65 -0
- package/templates/docs/E2E_TESTING.md +52 -7
- package/templates/e2e/fixtures/index.ts +13 -2
- package/templates/package.json +7 -3
- package/templates/playwright.config.ts +6 -1
- package/templates/src/components/layout/Header.tsx +2 -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/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 +4 -2
- package/templates/src/contexts/clerkContext.tsx +45 -0
- package/templates/src/hooks/index.ts +23 -2
- 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/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/index.ts +1 -0
- package/templates/src/lib/sentry.ts +55 -0
- package/templates/src/lib/storage.ts +6 -2
- package/templates/src/main.tsx +18 -8
- package/templates/src/stores/preferencesStore.ts +34 -9
- package/templates/src/test/clerkMock.tsx +97 -0
- package/templates/src/test/index.ts +3 -0
- package/templates/src/test/providers.tsx +7 -4
- package/templates/src/test-setup.ts +16 -2
- 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":"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"}
|
package/package.json
CHANGED
package/templates/.env.example
CHANGED
|
@@ -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:
|
|
88
|
-
project:
|
|
89
|
-
command: npx playwright test --project=
|
|
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
|
package/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
-
<
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
-
|
|
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,
|
|
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
|
|
111
|
-
npm run e2e:
|
|
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
|
+
}
|
package/templates/package.json
CHANGED
|
@@ -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=
|
|
43
|
-
"e2e:
|
|
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: '
|
|
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
|
|
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: /
|
|
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
|
|
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: /
|
|
122
|
+
fireEvent.click(screen.getByRole('button', { name: /reload page/i }));
|
|
123
123
|
|
|
124
124
|
expect(reloadMock).toHaveBeenCalledTimes(1);
|
|
125
125
|
|