@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.
Files changed (70) hide show
  1. package/README.md +2 -1
  2. package/dist/features/index.d.ts +0 -1
  3. package/dist/features/index.d.ts.map +1 -1
  4. package/dist/features/index.js +0 -1
  5. package/dist/features/index.js.map +1 -1
  6. package/dist/features/registry.d.ts +4 -0
  7. package/dist/features/registry.d.ts.map +1 -1
  8. package/dist/features/registry.js +144 -113
  9. package/dist/features/registry.js.map +1 -1
  10. package/dist/features/types.d.ts +12 -2
  11. package/dist/features/types.d.ts.map +1 -1
  12. package/dist/tools/get-example.d.ts.map +1 -1
  13. package/dist/tools/get-example.js +3 -2
  14. package/dist/tools/get-example.js.map +1 -1
  15. package/dist/tools/get-features.d.ts.map +1 -1
  16. package/dist/tools/get-features.js +2 -1
  17. package/dist/tools/get-features.js.map +1 -1
  18. package/dist/tools/get-scaffold.d.ts.map +1 -1
  19. package/dist/tools/get-scaffold.js +12 -64
  20. package/dist/tools/get-scaffold.js.map +1 -1
  21. package/dist/utils/examples.d.ts.map +1 -1
  22. package/dist/utils/examples.js +40 -4
  23. package/dist/utils/examples.js.map +1 -1
  24. package/dist/utils/scaffold.d.ts +9 -5
  25. package/dist/utils/scaffold.d.ts.map +1 -1
  26. package/dist/utils/scaffold.js +81 -17
  27. package/dist/utils/scaffold.js.map +1 -1
  28. package/package.json +1 -1
  29. package/templates/.github/workflows/ci.yml +35 -63
  30. package/templates/.github/workflows/release.yml +36 -0
  31. package/templates/CLAUDE.md +5 -3
  32. package/templates/docs/ARCHITECTURE.md +0 -1
  33. package/templates/docs/COMPONENT_GUIDELINES.md +1 -1
  34. package/templates/docs/E2E_TESTING.md +22 -7
  35. package/templates/docs/TESTING.md +2 -14
  36. package/templates/docs/WORKFLOW.md +5 -5
  37. package/templates/e2e/performance/home.spec.ts +94 -0
  38. package/templates/e2e/performance/setup.ts +9 -0
  39. package/templates/e2e/tests/home.spec.ts +2 -2
  40. package/templates/gitignore +1 -1
  41. package/templates/package.json +19 -8
  42. package/templates/playwright.config.ts +15 -3
  43. package/templates/src/contexts/performanceContext.test.tsx +51 -0
  44. package/templates/src/contexts/performanceContext.tsx +64 -0
  45. package/templates/src/main.tsx +5 -2
  46. package/templates/src/pages/Home.tsx +38 -33
  47. package/templates/src/test/providers.tsx +5 -1
  48. package/templates/vitest.config.ts +1 -1
  49. package/templates/lighthouse-budget.json +0 -17
  50. package/templates/lighthouserc.json +0 -23
  51. /package/templates/{tests/unit/components → src/components/layout}/Header.test.tsx +0 -0
  52. /package/templates/{tests/unit/components → src/components/shared/ErrorBoundary}/ErrorBoundary.test.tsx +0 -0
  53. /package/templates/{tests/unit/components → src/components/shared/LanguageSwitcher}/LanguageSwitcher.test.tsx +0 -0
  54. /package/templates/{tests/unit/components → src/components/shared/RegisterForm}/RegisterForm.test.tsx +0 -0
  55. /package/templates/{tests/unit/components → src/components/shared/SEO}/SEO.test.tsx +0 -0
  56. /package/templates/{tests/unit/components → src/components/shared/ThemeToggle}/ThemeToggle.test.tsx +0 -0
  57. /package/templates/{tests/unit/components/Loading.test.tsx → src/components/ui/loading.test.tsx} +0 -0
  58. /package/templates/{tests/unit → src}/contexts/mobileContext.test.tsx +0 -0
  59. /package/templates/{tests/unit → src}/hooks/useExampleQuery.test.tsx +0 -0
  60. /package/templates/{tests/unit → src}/hooks/useLanguage.test.tsx +0 -0
  61. /package/templates/{tests/unit → src}/hooks/useMediaQuery.test.ts +0 -0
  62. /package/templates/{tests/unit → src}/hooks/useRegisterForm.test.tsx +0 -0
  63. /package/templates/{tests/unit → src}/hooks/useThemeEffect.test.ts +0 -0
  64. /package/templates/{tests/unit → src}/i18n/detectLanguage.test.ts +0 -0
  65. /package/templates/{tests/unit → src}/i18n/loadCatalog.test.ts +0 -0
  66. /package/templates/{tests/unit → src}/lib/api.test.ts +0 -0
  67. /package/templates/{tests/unit → src}/lib/storage.test.ts +0 -0
  68. /package/templates/{tests/unit → src}/lib/utils.test.ts +0 -0
  69. /package/templates/{tests/unit → src}/lib/validations.test.ts +0 -0
  70. /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: [lint, typecheck]
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 }}-${{ hashFiles('package-lock.json') }}
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 chromium --with-deps
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
- - run: npm run e2e
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: playwright-report
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 }}
@@ -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 in `tests/unit/` mirror `src/` structure. 80% coverage required.
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';
@@ -35,7 +35,6 @@ src/
35
35
  ├── test/ # Test utilities and providers
36
36
  └── types/ # Shared TypeScript definitions
37
37
 
38
- tests/unit/ # Vitest tests (mirrors src/ structure)
39
38
  e2e/ # Playwright end-to-end tests
40
39
  ```
41
40
 
@@ -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 in `tests/unit/`
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
- └── 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
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 # Run all
108
- npm run e2e:ui # Interactive 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**: `tests/unit/` mirroring `src/` structure
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
- ```typescript
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 go in `tests/unit/` mirroring `src/`:
74
+ Tests are co-located with source files:
75
75
 
76
- | Source | Test |
77
- | --------------------------- | --------------------------------------- |
78
- | `src/hooks/useAuth.ts` | `tests/unit/hooks/useAuth.test.ts` |
79
- | `src/components/Button.tsx` | `tests/unit/components/Button.test.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
- // Click skip link to navigate to main content
37
- await skipLink.click();
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();
@@ -11,7 +11,7 @@ dist-ssr/
11
11
  .idea/
12
12
  *.swp
13
13
  *.swo
14
- .DS_Stores
14
+ .DS_Store
15
15
 
16
16
  # Environment
17
17
  .env
@@ -22,7 +22,7 @@
22
22
  "shadcn",
23
23
  "tailwind"
24
24
  ],
25
- "version": "0.5.0",
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/tests',
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: 'chromium',
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
+ }