@react-spa-scaffold/mcp 1.1.0 → 1.1.2
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/features/index.d.ts +0 -1
- package/dist/features/index.d.ts.map +1 -1
- package/dist/features/index.js +0 -1
- package/dist/features/index.js.map +1 -1
- package/dist/features/registry.d.ts +4 -0
- package/dist/features/registry.d.ts.map +1 -1
- package/dist/features/registry.js +144 -113
- package/dist/features/registry.js.map +1 -1
- package/dist/features/types.d.ts +12 -2
- package/dist/features/types.d.ts.map +1 -1
- package/dist/tools/get-example.d.ts.map +1 -1
- package/dist/tools/get-example.js +3 -2
- package/dist/tools/get-example.js.map +1 -1
- package/dist/tools/get-features.d.ts.map +1 -1
- package/dist/tools/get-features.js +2 -1
- package/dist/tools/get-features.js.map +1 -1
- package/dist/tools/get-scaffold.d.ts.map +1 -1
- package/dist/tools/get-scaffold.js +12 -64
- package/dist/tools/get-scaffold.js.map +1 -1
- package/dist/utils/examples.d.ts.map +1 -1
- package/dist/utils/examples.js +40 -4
- package/dist/utils/examples.js.map +1 -1
- package/dist/utils/scaffold.d.ts +9 -5
- package/dist/utils/scaffold.d.ts.map +1 -1
- package/dist/utils/scaffold.js +81 -17
- package/dist/utils/scaffold.js.map +1 -1
- package/package.json +1 -1
- package/templates/.github/workflows/ci.yml +35 -63
- package/templates/.github/workflows/release.yml +36 -0
- package/templates/CLAUDE.md +5 -3
- package/templates/docs/ARCHITECTURE.md +0 -1
- package/templates/docs/COMPONENT_GUIDELINES.md +1 -1
- package/templates/docs/E2E_TESTING.md +22 -7
- package/templates/docs/TESTING.md +2 -14
- package/templates/docs/WORKFLOW.md +5 -5
- package/templates/e2e/performance/home.spec.ts +94 -0
- package/templates/e2e/performance/setup.ts +9 -0
- package/templates/e2e/tests/home.spec.ts +2 -2
- package/templates/gitignore +1 -1
- package/templates/package.json +19 -8
- package/templates/playwright.config.ts +15 -3
- package/templates/src/contexts/performanceContext.test.tsx +51 -0
- package/templates/src/contexts/performanceContext.tsx +64 -0
- package/templates/src/main.tsx +5 -2
- package/templates/src/pages/Home.tsx +38 -33
- package/templates/src/test/providers.tsx +5 -1
- package/templates/vitest.config.ts +1 -1
- package/templates/lighthouse-budget.json +0 -17
- package/templates/lighthouserc.json +0 -23
- /package/templates/{tests/unit/components → src/components/layout}/Header.test.tsx +0 -0
- /package/templates/{tests/unit/components → src/components/shared/ErrorBoundary}/ErrorBoundary.test.tsx +0 -0
- /package/templates/{tests/unit/components → src/components/shared/LanguageSwitcher}/LanguageSwitcher.test.tsx +0 -0
- /package/templates/{tests/unit/components → src/components/shared/RegisterForm}/RegisterForm.test.tsx +0 -0
- /package/templates/{tests/unit/components → src/components/shared/SEO}/SEO.test.tsx +0 -0
- /package/templates/{tests/unit/components → src/components/shared/ThemeToggle}/ThemeToggle.test.tsx +0 -0
- /package/templates/{tests/unit/components/Loading.test.tsx → src/components/ui/loading.test.tsx} +0 -0
- /package/templates/{tests/unit → src}/contexts/mobileContext.test.tsx +0 -0
- /package/templates/{tests/unit → src}/hooks/useExampleQuery.test.tsx +0 -0
- /package/templates/{tests/unit → src}/hooks/useLanguage.test.tsx +0 -0
- /package/templates/{tests/unit → src}/hooks/useMediaQuery.test.ts +0 -0
- /package/templates/{tests/unit → src}/hooks/useRegisterForm.test.tsx +0 -0
- /package/templates/{tests/unit → src}/hooks/useThemeEffect.test.ts +0 -0
- /package/templates/{tests/unit → src}/i18n/detectLanguage.test.ts +0 -0
- /package/templates/{tests/unit → src}/i18n/loadCatalog.test.ts +0 -0
- /package/templates/{tests/unit → src}/lib/api.test.ts +0 -0
- /package/templates/{tests/unit → src}/lib/storage.test.ts +0 -0
- /package/templates/{tests/unit → src}/lib/utils.test.ts +0 -0
- /package/templates/{tests/unit → src}/lib/validations.test.ts +0 -0
- /package/templates/{tests/unit → src}/stores/preferencesStore.test.ts +0 -0
|
@@ -5,8 +5,6 @@ on:
|
|
|
5
5
|
branches: [main, master]
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: [main, master]
|
|
8
|
-
release:
|
|
9
|
-
types: [published]
|
|
10
8
|
|
|
11
9
|
concurrency:
|
|
12
10
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
@@ -76,81 +74,55 @@ jobs:
|
|
|
76
74
|
path: coverage/
|
|
77
75
|
retention-days: 14
|
|
78
76
|
|
|
79
|
-
e2e:
|
|
80
|
-
name: E2E Tests
|
|
81
|
-
needs: [
|
|
77
|
+
test-e2e:
|
|
78
|
+
name: E2E Tests (${{ matrix.type }})
|
|
79
|
+
needs: [build]
|
|
82
80
|
runs-on: ubuntu-latest
|
|
83
81
|
timeout-minutes: 15
|
|
82
|
+
continue-on-error: ${{ matrix.type == 'performance' }}
|
|
83
|
+
strategy:
|
|
84
|
+
fail-fast: false
|
|
85
|
+
matrix:
|
|
86
|
+
include:
|
|
87
|
+
- type: functional
|
|
88
|
+
project: functional
|
|
89
|
+
command: npx playwright test --project=functional
|
|
90
|
+
report-name: playwright-report
|
|
91
|
+
upload-on: failure
|
|
92
|
+
- type: performance
|
|
93
|
+
project: performance
|
|
94
|
+
command: PERF_TEST=true npx playwright test --project=performance
|
|
95
|
+
report-name: performance-report
|
|
96
|
+
upload-on: always
|
|
84
97
|
steps:
|
|
85
98
|
- uses: actions/checkout@v6
|
|
86
99
|
- uses: ./.github/actions/setup-node-deps
|
|
100
|
+
- uses: actions/download-artifact@v6
|
|
101
|
+
with:
|
|
102
|
+
name: dist
|
|
103
|
+
path: dist/
|
|
104
|
+
- name: Get Playwright version
|
|
105
|
+
id: playwright-version
|
|
106
|
+
run: echo "version=$(npm ls @playwright/test --json | jq -r '.dependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT
|
|
87
107
|
- name: Cache Playwright browsers
|
|
88
108
|
uses: actions/cache@v5
|
|
89
109
|
id: playwright-cache
|
|
90
110
|
with:
|
|
91
111
|
path: ~/.cache/ms-playwright
|
|
92
|
-
key: playwright-${{ runner.os }}-${{
|
|
112
|
+
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
|
|
93
113
|
- name: Install Playwright browsers
|
|
94
114
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
95
|
-
run: npx playwright install
|
|
96
|
-
- name: Install Playwright deps (cached)
|
|
115
|
+
run: npx playwright install --with-deps chromium
|
|
116
|
+
- name: Install Playwright deps (when cached)
|
|
97
117
|
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
|
98
118
|
run: npx playwright install-deps chromium
|
|
99
|
-
-
|
|
119
|
+
- name: Run ${{ matrix.type }} tests
|
|
120
|
+
run: ${{ matrix.command }}
|
|
121
|
+
env:
|
|
122
|
+
CI: true
|
|
100
123
|
- uses: actions/upload-artifact@v6
|
|
101
|
-
if: failure()
|
|
124
|
+
if: ${{ matrix.upload-on == 'always' || failure() }}
|
|
102
125
|
with:
|
|
103
|
-
name:
|
|
126
|
+
name: ${{ matrix.report-name }}
|
|
104
127
|
path: playwright-report/
|
|
105
|
-
retention-days: 7
|
|
106
|
-
|
|
107
|
-
lighthouse:
|
|
108
|
-
name: Lighthouse CI
|
|
109
|
-
needs: [build]
|
|
110
|
-
runs-on: ubuntu-latest
|
|
111
|
-
timeout-minutes: 10
|
|
112
|
-
continue-on-error: true
|
|
113
|
-
steps:
|
|
114
|
-
- uses: actions/checkout@v6
|
|
115
|
-
- uses: ./.github/actions/setup-node-deps
|
|
116
|
-
- uses: actions/download-artifact@v7
|
|
117
|
-
with:
|
|
118
|
-
name: dist
|
|
119
|
-
path: dist/
|
|
120
|
-
- name: Run Lighthouse CI
|
|
121
|
-
uses: treosh/lighthouse-ci-action@v12
|
|
122
|
-
with:
|
|
123
|
-
configPath: ./lighthouserc.json
|
|
124
|
-
uploadArtifacts: true
|
|
125
|
-
temporaryPublicStorage: true
|
|
126
|
-
- uses: actions/upload-artifact@v6
|
|
127
|
-
if: always()
|
|
128
|
-
with:
|
|
129
|
-
name: lighthouse-report
|
|
130
|
-
path: .lighthouseci/
|
|
131
|
-
retention-days: 14
|
|
132
|
-
|
|
133
|
-
publish:
|
|
134
|
-
name: Publish to npm
|
|
135
|
-
needs: [build, test]
|
|
136
|
-
if: github.event_name == 'release'
|
|
137
|
-
runs-on: ubuntu-latest
|
|
138
|
-
timeout-minutes: 15
|
|
139
|
-
permissions:
|
|
140
|
-
contents: read
|
|
141
|
-
id-token: write
|
|
142
|
-
steps:
|
|
143
|
-
- uses: actions/checkout@v6
|
|
144
|
-
- uses: ./.github/actions/setup-node-deps
|
|
145
|
-
- name: Configure npm authentication
|
|
146
|
-
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
|
147
|
-
env:
|
|
148
|
-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
149
|
-
- name: Publish @react-spa-scaffold/tsconfig
|
|
150
|
-
run: npm publish -w @react-spa-scaffold/tsconfig --provenance --access public || echo "Package may already exist at this version"
|
|
151
|
-
- name: Publish @react-spa-scaffold/eslint-config
|
|
152
|
-
run: npm publish -w @react-spa-scaffold/eslint-config --provenance --access public || echo "Package may already exist at this version"
|
|
153
|
-
- name: Publish @react-spa-scaffold/prettier-config
|
|
154
|
-
run: npm publish -w @react-spa-scaffold/prettier-config --provenance --access public || echo "Package may already exist at this version"
|
|
155
|
-
- name: Publish @react-spa-scaffold/mcp
|
|
156
|
-
run: npm publish -w @react-spa-scaffold/mcp --provenance --access public || echo "Package may already exist at this version"
|
|
128
|
+
retention-days: ${{ matrix.type == 'performance' && 14 || 7 }}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
|
|
8
|
+
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release:
|
|
12
|
+
name: Release
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
timeout-minutes: 15
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
pull-requests: write
|
|
18
|
+
id-token: write
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout
|
|
21
|
+
uses: actions/checkout@v6
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0
|
|
24
|
+
|
|
25
|
+
- uses: ./.github/actions/setup-node-deps
|
|
26
|
+
|
|
27
|
+
- name: Create Release Pull Request or Publish
|
|
28
|
+
uses: changesets/action@v1
|
|
29
|
+
with:
|
|
30
|
+
version: npm run version
|
|
31
|
+
publish: npm run release
|
|
32
|
+
commit: 'chore: version packages'
|
|
33
|
+
title: 'chore: version packages'
|
|
34
|
+
env:
|
|
35
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
36
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/templates/CLAUDE.md
CHANGED
|
@@ -14,7 +14,8 @@ 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 E2E
|
|
17
|
+
npm run e2e # Playwright functional E2E tests
|
|
18
|
+
npm run e2e:perf # Performance regression tests
|
|
18
19
|
npm run i18n:extract # Extract translations to .po
|
|
19
20
|
```
|
|
20
21
|
|
|
@@ -32,10 +33,11 @@ src/
|
|
|
32
33
|
├── stores/ # Zustand stores
|
|
33
34
|
└── types/ # TypeScript definitions
|
|
34
35
|
|
|
35
|
-
tests/unit/ # Vitest (mirrors src/)
|
|
36
36
|
e2e/ # Playwright tests
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
**Tests**: Co-located with source files (e.g., `foo.tsx` + `foo.test.tsx`)
|
|
40
|
+
|
|
39
41
|
## Code Patterns
|
|
40
42
|
|
|
41
43
|
**Imports**: Always use `@/` path alias
|
|
@@ -128,7 +130,7 @@ See [docs/INTERNATIONALIZATION.md](docs/INTERNATIONALIZATION.md) for full detail
|
|
|
128
130
|
|
|
129
131
|
See [docs/TESTING.md](docs/TESTING.md) and [docs/E2E_TESTING.md](docs/E2E_TESTING.md).
|
|
130
132
|
|
|
131
|
-
Tests
|
|
133
|
+
Tests are co-located with source files (e.g., `foo.tsx` + `foo.test.tsx`). 80% coverage required.
|
|
132
134
|
|
|
133
135
|
```typescript
|
|
134
136
|
import { describe, it, expect, vi } from 'vitest';
|
|
@@ -298,4 +298,4 @@ Before submitting a component:
|
|
|
298
298
|
- [ ] Uses `cn()` for className merging
|
|
299
299
|
- [ ] Accessible (roles, aria-labels, keyboard nav)
|
|
300
300
|
- [ ] Barrel export in `index.ts` (except UI primitives)
|
|
301
|
-
- [ ] Test file
|
|
301
|
+
- [ ] Test file co-located (e.g., `Button.test.tsx`)
|
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
e2e/
|
|
13
13
|
├── fixtures/
|
|
14
14
|
│ └── index.ts # setupPage, clearAppState
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
├── tests/ # Functional E2E tests
|
|
16
|
+
│ ├── home.spec.ts # Page structure, accessibility
|
|
17
|
+
│ ├── theme.spec.ts # Theme toggle, persistence
|
|
18
|
+
│ ├── language.spec.ts # Language switcher
|
|
19
|
+
│ └── navigation.spec.ts # Routing, 404
|
|
20
|
+
└── performance/ # Performance regression tests
|
|
21
|
+
├── setup.ts # Performance test fixture
|
|
22
|
+
└── home.spec.ts # Home page performance tests
|
|
20
23
|
```
|
|
21
24
|
|
|
22
25
|
## Imports
|
|
@@ -104,10 +107,22 @@ await page.waitForTimeout(500);
|
|
|
104
107
|
## Running Tests
|
|
105
108
|
|
|
106
109
|
```bash
|
|
107
|
-
npm run e2e
|
|
108
|
-
npm run e2e:ui
|
|
110
|
+
npm run e2e # Run functional tests
|
|
111
|
+
npm run e2e:ui # Functional tests with interactive UI
|
|
112
|
+
npm run e2e:perf # Run performance tests
|
|
113
|
+
npm run e2e:perf:ui # Performance tests with interactive UI
|
|
114
|
+
npm run e2e:all # Run all tests (functional + performance)
|
|
109
115
|
```
|
|
110
116
|
|
|
117
|
+
## Performance Testing
|
|
118
|
+
|
|
119
|
+
Performance tests use [react-performance-tracking](https://github.com/mkaczkowski/react-performance-tracking) to measure:
|
|
120
|
+
|
|
121
|
+
- React Profiler metrics (render duration, re-renders)
|
|
122
|
+
- Lighthouse audits (performance, accessibility)
|
|
123
|
+
- Core Web Vitals (LCP, INP, CLS)
|
|
124
|
+
- FPS monitoring (Chromium only)
|
|
125
|
+
|
|
111
126
|
## Checklist
|
|
112
127
|
|
|
113
128
|
- [ ] Uses accessible selectors
|
|
@@ -4,19 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
- **Framework**: Vitest + React Testing Library
|
|
6
6
|
- **Coverage threshold**: 80% (lines, branches, functions, statements)
|
|
7
|
-
- **Test location**: `
|
|
8
|
-
|
|
9
|
-
## File Structure
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
tests/unit/
|
|
13
|
-
├── components/ # Component tests
|
|
14
|
-
├── hooks/ # Hook tests
|
|
15
|
-
├── lib/ # Utility function tests
|
|
16
|
-
├── stores/ # Zustand store tests
|
|
17
|
-
├── contexts/ # Context provider tests
|
|
18
|
-
└── i18n/ # Internationalization tests
|
|
19
|
-
```
|
|
7
|
+
- **Test location**: Co-located with source files (e.g., `Button.tsx` + `Button.test.tsx`)
|
|
20
8
|
|
|
21
9
|
## Naming Conventions
|
|
22
10
|
|
|
@@ -85,7 +73,7 @@ describe('useMediaQuery', () => {
|
|
|
85
73
|
|
|
86
74
|
### Component Testing
|
|
87
75
|
|
|
88
|
-
```
|
|
76
|
+
```tsx
|
|
89
77
|
import { screen } from '@testing-library/react';
|
|
90
78
|
import { render } from '@/test';
|
|
91
79
|
|
|
@@ -71,12 +71,12 @@ The `tdd-workflow` skill (`.claude/skills/tdd-workflow/SKILL.md`) provides TDD g
|
|
|
71
71
|
|
|
72
72
|
### Test Location
|
|
73
73
|
|
|
74
|
-
Tests
|
|
74
|
+
Tests are co-located with source files:
|
|
75
75
|
|
|
76
|
-
| Source | Test
|
|
77
|
-
| --------------------------- |
|
|
78
|
-
| `src/hooks/useAuth.ts` | `
|
|
79
|
-
| `src/components/Button.tsx` | `
|
|
76
|
+
| Source | Test |
|
|
77
|
+
| --------------------------- | -------------------------------- |
|
|
78
|
+
| `src/hooks/useAuth.ts` | `src/hooks/useAuth.test.ts` |
|
|
79
|
+
| `src/components/Button.tsx` | `src/components/Button.test.tsx` |
|
|
80
80
|
|
|
81
81
|
### Test Patterns
|
|
82
82
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { expect, test } from './setup';
|
|
2
|
+
|
|
3
|
+
test.describe('Home Page Performance', () => {
|
|
4
|
+
// React Profiler + FPS metrics (Chromium only for FPS - uses Chrome DevTools Protocol)
|
|
5
|
+
test.performance({
|
|
6
|
+
warmup: true,
|
|
7
|
+
iterations: 3,
|
|
8
|
+
thresholds: {
|
|
9
|
+
base: {
|
|
10
|
+
profiler: {
|
|
11
|
+
'home-page': { duration: 200, rerenders: 10 },
|
|
12
|
+
},
|
|
13
|
+
fps: 55,
|
|
14
|
+
},
|
|
15
|
+
ci: {
|
|
16
|
+
profiler: {
|
|
17
|
+
'home-page': { duration: 300 },
|
|
18
|
+
},
|
|
19
|
+
fps: 45,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
})('initial page load', async ({ page, performance }) => {
|
|
23
|
+
await page.goto('/');
|
|
24
|
+
await performance.init();
|
|
25
|
+
|
|
26
|
+
await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test.performance({
|
|
30
|
+
warmup: true,
|
|
31
|
+
throttleRate: 4, // 4x CPU slowdown
|
|
32
|
+
networkThrottling: 'fast-3g',
|
|
33
|
+
thresholds: {
|
|
34
|
+
base: {
|
|
35
|
+
profiler: {
|
|
36
|
+
'home-page': { duration: 1200, rerenders: 10 },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
})('page load under throttled conditions', async ({ page, performance }) => {
|
|
41
|
+
await page.goto('/');
|
|
42
|
+
await performance.init();
|
|
43
|
+
|
|
44
|
+
await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Lighthouse audits (Chromium only - requires lighthouse peer dependency)
|
|
48
|
+
test.performance({
|
|
49
|
+
thresholds: {
|
|
50
|
+
base: {
|
|
51
|
+
lighthouse: {
|
|
52
|
+
performance: 80,
|
|
53
|
+
accessibility: 90,
|
|
54
|
+
bestPractices: 80,
|
|
55
|
+
seo: 90,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
ci: {
|
|
59
|
+
lighthouse: {
|
|
60
|
+
performance: 70, // Relaxed for CI
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
})('meets Lighthouse quality standards', async ({ page, performance }) => {
|
|
65
|
+
await page.goto('/');
|
|
66
|
+
await performance.init();
|
|
67
|
+
|
|
68
|
+
await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Web Vitals with comprehensive metrics
|
|
72
|
+
test.performance({
|
|
73
|
+
thresholds: {
|
|
74
|
+
base: {
|
|
75
|
+
profiler: {
|
|
76
|
+
'home-page': { duration: 200, rerenders: 10 },
|
|
77
|
+
},
|
|
78
|
+
webVitals: {
|
|
79
|
+
lcp: 2500, // Largest Contentful Paint (ms)
|
|
80
|
+
inp: 200, // Interaction to Next Paint (ms)
|
|
81
|
+
cls: 0.1, // Cumulative Layout Shift
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
})('meets Core Web Vitals standards', async ({ page, performance }) => {
|
|
86
|
+
await page.goto('/');
|
|
87
|
+
await performance.init();
|
|
88
|
+
|
|
89
|
+
// Trigger interaction for INP measurement
|
|
90
|
+
await page.getByRole('heading', { name: /welcome/i }).click();
|
|
91
|
+
|
|
92
|
+
await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { test as base } from '@playwright/test';
|
|
2
|
+
import { createPerformanceTest } from 'react-performance-tracking/playwright';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Playwright test extended with performance fixture.
|
|
6
|
+
* @see https://github.com/mkaczkowski/react-performance-tracking
|
|
7
|
+
*/
|
|
8
|
+
export const test = createPerformanceTest(base);
|
|
9
|
+
export { expect } from '@playwright/test';
|
|
@@ -33,8 +33,8 @@ test.describe('Home Page', () => {
|
|
|
33
33
|
// Verify skip link becomes visible when focused
|
|
34
34
|
await expect(skipLink).toBeVisible();
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
await skipLink.
|
|
36
|
+
// Activate skip link with keyboard (how real users interact with skip links)
|
|
37
|
+
await skipLink.press('Enter');
|
|
38
38
|
|
|
39
39
|
// Main should be scrolled into view
|
|
40
40
|
await expect(page.locator('#main')).toBeInViewport();
|
package/templates/gitignore
CHANGED
package/templates/package.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"shadcn",
|
|
23
23
|
"tailwind"
|
|
24
24
|
],
|
|
25
|
-
"version": "0.5.
|
|
25
|
+
"version": "0.5.1",
|
|
26
26
|
"type": "module",
|
|
27
27
|
"workspaces": [
|
|
28
28
|
"packages/*"
|
|
@@ -39,14 +39,20 @@
|
|
|
39
39
|
"test": "vitest run",
|
|
40
40
|
"test:watch": "vitest",
|
|
41
41
|
"test:coverage": "vitest run --coverage",
|
|
42
|
-
"e2e": "playwright test",
|
|
43
|
-
"e2e:ui": "playwright test --ui",
|
|
42
|
+
"e2e": "playwright test --project=functional",
|
|
43
|
+
"e2e:ui": "playwright test --project=functional --ui",
|
|
44
|
+
"e2e:perf": "PERF_TEST=true playwright test --project=performance",
|
|
45
|
+
"e2e:perf:ui": "PERF_TEST=true playwright test --project=performance --ui",
|
|
46
|
+
"e2e:all": "PERF_TEST=true playwright test",
|
|
44
47
|
"i18n:extract": "lingui extract",
|
|
45
48
|
"prepare": "husky",
|
|
46
49
|
"mcp:build": "npm run build -w @react-spa-scaffold/mcp",
|
|
47
50
|
"mcp:dev": "npm run dev -w @react-spa-scaffold/mcp",
|
|
48
51
|
"mcp:start": "npm run start -w @react-spa-scaffold/mcp",
|
|
49
|
-
"mcp:inspect": "npm run inspect -w @react-spa-scaffold/mcp"
|
|
52
|
+
"mcp:inspect": "npm run inspect -w @react-spa-scaffold/mcp",
|
|
53
|
+
"changeset": "changeset",
|
|
54
|
+
"version": "changeset version",
|
|
55
|
+
"release": "changeset publish"
|
|
50
56
|
},
|
|
51
57
|
"dependencies": {
|
|
52
58
|
"@fontsource-variable/inter": "^5.2.5",
|
|
@@ -63,6 +69,7 @@
|
|
|
63
69
|
"react": "^19.1.0",
|
|
64
70
|
"react-dom": "^19.1.0",
|
|
65
71
|
"react-hook-form": "^7.58.0",
|
|
72
|
+
"react-performance-tracking": "^1.2.1",
|
|
66
73
|
"react-router": "^7.11.0",
|
|
67
74
|
"sonner": "^2.0.7",
|
|
68
75
|
"tailwind-merge": "^3.3.0",
|
|
@@ -71,16 +78,17 @@
|
|
|
71
78
|
"zustand": "^5.0.9"
|
|
72
79
|
},
|
|
73
80
|
"devDependencies": {
|
|
81
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
82
|
+
"@changesets/cli": "^2.29.8",
|
|
74
83
|
"@commitlint/config-conventional": "^20.2.0",
|
|
75
|
-
"@react-spa-scaffold/eslint-config": "*",
|
|
76
|
-
"@react-spa-scaffold/prettier-config": "*",
|
|
77
|
-
"@react-spa-scaffold/tsconfig": "*",
|
|
78
|
-
"shadcn": "^3.6.2",
|
|
79
84
|
"@eslint/js": "^9.28.0",
|
|
80
85
|
"@lingui/babel-plugin-lingui-macro": "^5.7.0",
|
|
81
86
|
"@lingui/cli": "^5.7.0",
|
|
82
87
|
"@lingui/vite-plugin": "^5.7.0",
|
|
83
88
|
"@playwright/test": "^1.52.0",
|
|
89
|
+
"@react-spa-scaffold/eslint-config": "*",
|
|
90
|
+
"@react-spa-scaffold/prettier-config": "*",
|
|
91
|
+
"@react-spa-scaffold/tsconfig": "*",
|
|
84
92
|
"@sentry/vite-plugin": "^4.6.1",
|
|
85
93
|
"@tailwindcss/vite": "^4.1.17",
|
|
86
94
|
"@testing-library/jest-dom": "^6.6.3",
|
|
@@ -92,6 +100,7 @@
|
|
|
92
100
|
"@vitejs/plugin-react": "^5.1.2",
|
|
93
101
|
"@vitest/coverage-v8": "^4.0.16",
|
|
94
102
|
"babel-plugin-macros": "^3.1.0",
|
|
103
|
+
"chrome-launcher": "^1.2.1",
|
|
95
104
|
"commitlint": "^20.2.0",
|
|
96
105
|
"eslint": "^9.28.0",
|
|
97
106
|
"eslint-config-prettier": "^10.1.0",
|
|
@@ -100,10 +109,12 @@
|
|
|
100
109
|
"eslint-plugin-react-refresh": "^0.4.20",
|
|
101
110
|
"husky": "^9.1.7",
|
|
102
111
|
"jsdom": "^27.4.0",
|
|
112
|
+
"lighthouse": "^12.8.2",
|
|
103
113
|
"lint-staged": "^16.1.0",
|
|
104
114
|
"msw": "^2.12.7",
|
|
105
115
|
"prettier": "^3.5.3",
|
|
106
116
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
117
|
+
"shadcn": "^3.6.2",
|
|
107
118
|
"tailwindcss": "^4.1.17",
|
|
108
119
|
"typescript": "~5.9.0",
|
|
109
120
|
"typescript-eslint": "^8.33.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineConfig, devices } from '@playwright/test';
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
|
-
testDir: './e2e
|
|
4
|
+
testDir: './e2e',
|
|
5
5
|
fullyParallel: true,
|
|
6
6
|
forbidOnly: !!process.env.CI,
|
|
7
7
|
retries: process.env.CI ? 2 : 0,
|
|
@@ -17,12 +17,24 @@ export default defineConfig({
|
|
|
17
17
|
},
|
|
18
18
|
projects: [
|
|
19
19
|
{
|
|
20
|
-
name: '
|
|
20
|
+
name: 'functional',
|
|
21
|
+
testDir: './e2e/tests',
|
|
21
22
|
use: { ...devices['Desktop Chrome'] },
|
|
22
23
|
},
|
|
24
|
+
{
|
|
25
|
+
name: 'performance',
|
|
26
|
+
testDir: './e2e/performance',
|
|
27
|
+
use: {
|
|
28
|
+
...devices['Desktop Chrome'],
|
|
29
|
+
// CI containers require --no-sandbox; --disable-dev-shm-usage prevents memory issues
|
|
30
|
+
launchOptions: {
|
|
31
|
+
args: process.env.CI ? ['--no-sandbox', '--disable-dev-shm-usage'] : [],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
23
35
|
],
|
|
24
36
|
webServer: {
|
|
25
|
-
command: 'npm run dev',
|
|
37
|
+
command: process.env.PERF_TEST ? 'VITE_PERF_TEST=true npm run dev' : 'npm run dev',
|
|
26
38
|
url: 'http://localhost:5173',
|
|
27
39
|
reuseExistingServer: !process.env.CI,
|
|
28
40
|
timeout: 120000,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { PerformanceProviderWrapper, usePerformance } from '@/contexts/performanceContext';
|
|
6
|
+
|
|
7
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
8
|
+
<PerformanceProviderWrapper>{children}</PerformanceProviderWrapper>
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
describe('usePerformance', () => {
|
|
12
|
+
it('returns no-op callback when used outside provider', () => {
|
|
13
|
+
// Should NOT throw, unlike usePerformanceRequired from the library
|
|
14
|
+
const { result } = renderHook(() => usePerformance());
|
|
15
|
+
|
|
16
|
+
expect(result.current).toBeDefined();
|
|
17
|
+
expect(result.current.onProfilerRender).toBeInstanceOf(Function);
|
|
18
|
+
|
|
19
|
+
// Verify no-op callback doesn't throw when called
|
|
20
|
+
expect(() => {
|
|
21
|
+
result.current.onProfilerRender('test-id', 'mount', 100, 50, 1000, 1001);
|
|
22
|
+
}).not.toThrow();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns context value when used with provider', () => {
|
|
26
|
+
const { result } = renderHook(() => usePerformance(), { wrapper });
|
|
27
|
+
|
|
28
|
+
expect(result.current).toBeDefined();
|
|
29
|
+
expect(result.current.onProfilerRender).toBeInstanceOf(Function);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('PerformanceProviderWrapper', () => {
|
|
34
|
+
it('renders children when performance is disabled', () => {
|
|
35
|
+
// In test environment, DEV is false and VITE_PERF_TEST is not set
|
|
36
|
+
// So the provider should be disabled and just render children
|
|
37
|
+
const { result } = renderHook(() => usePerformance(), { wrapper });
|
|
38
|
+
|
|
39
|
+
expect(result.current).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not crash on mount or unmount', () => {
|
|
43
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
44
|
+
|
|
45
|
+
const { unmount } = renderHook(() => usePerformance(), { wrapper });
|
|
46
|
+
|
|
47
|
+
expect(() => unmount()).not.toThrow();
|
|
48
|
+
|
|
49
|
+
consoleSpy.mockRestore();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { lazy, Suspense, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { usePerformance as useLibPerformance } from 'react-performance-tracking/react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lazy-load the PerformanceProvider to avoid bundling it in production.
|
|
7
|
+
* This provider is only used during performance testing.
|
|
8
|
+
*/
|
|
9
|
+
const PerformanceProvider = lazy(() =>
|
|
10
|
+
import('react-performance-tracking/react').then((m) => ({
|
|
11
|
+
default: m.PerformanceProvider,
|
|
12
|
+
})),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
interface PerformanceProviderWrapperProps {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Conditionally wraps children with PerformanceProvider for E2E performance testing.
|
|
21
|
+
*
|
|
22
|
+
* Only enabled when:
|
|
23
|
+
* - Running in development mode (DEV), OR
|
|
24
|
+
* - VITE_PERF_TEST environment variable is set to "true"
|
|
25
|
+
*
|
|
26
|
+
* In production builds without the env var, this is a pass-through component
|
|
27
|
+
* with zero runtime overhead.
|
|
28
|
+
*
|
|
29
|
+
* @see https://github.com/mkaczkowski/react-performance-tracking
|
|
30
|
+
*/
|
|
31
|
+
export function PerformanceProviderWrapper({ children }: PerformanceProviderWrapperProps) {
|
|
32
|
+
const isPerformanceEnabled = import.meta.env.DEV || import.meta.env.VITE_PERF_TEST === 'true';
|
|
33
|
+
|
|
34
|
+
if (!isPerformanceEnabled) {
|
|
35
|
+
return <>{children}</>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Suspense fallback={<>{children}</>}>
|
|
40
|
+
<PerformanceProvider>{children}</PerformanceProvider>
|
|
41
|
+
</Suspense>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* No-op profiler callback for when performance tracking is disabled.
|
|
47
|
+
* This prevents crashes in production when the PerformanceProvider is not mounted.
|
|
48
|
+
*/
|
|
49
|
+
const noopProfilerCallback: React.ProfilerOnRenderCallback = () => {
|
|
50
|
+
// Intentionally empty - performance tracking disabled
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const noopContext = { onProfilerRender: noopProfilerCallback };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Safe hook that returns performance context or a no-op fallback.
|
|
57
|
+
* Unlike usePerformanceRequired from the library, this NEVER throws.
|
|
58
|
+
*/
|
|
59
|
+
export function usePerformance() {
|
|
60
|
+
const context = useLibPerformance();
|
|
61
|
+
|
|
62
|
+
// Return no-op if context is null (provider not mounted)
|
|
63
|
+
return context ?? noopContext;
|
|
64
|
+
}
|