@react-spa-scaffold/mcp 0.3.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 +423 -0
- package/dist/features/index.d.ts +5 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +3 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/registry.d.ts +10 -0
- package/dist/features/registry.d.ts.map +1 -0
- package/dist/features/registry.js +508 -0
- package/dist/features/registry.js.map +1 -0
- package/dist/features/types.d.ts +45 -0
- package/dist/features/types.d.ts.map +1 -0
- package/dist/features/types.js +5 -0
- package/dist/features/types.js.map +1 -0
- package/dist/features/versions.d.ts +16 -0
- package/dist/features/versions.d.ts.map +1 -0
- package/dist/features/versions.js +46 -0
- package/dist/features/versions.js.map +1 -0
- package/dist/features/versions.json +5 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/docs.d.ts +29 -0
- package/dist/resources/docs.d.ts.map +1 -0
- package/dist/resources/docs.js +105 -0
- package/dist/resources/docs.js.map +1 -0
- package/dist/resources/index.d.ts +2 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +2 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +115 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/get-example.d.ts +51 -0
- package/dist/tools/get-example.d.ts.map +1 -0
- package/dist/tools/get-example.js +90 -0
- package/dist/tools/get-example.js.map +1 -0
- package/dist/tools/get-features.d.ts +30 -0
- package/dist/tools/get-features.d.ts.map +1 -0
- package/dist/tools/get-features.js +46 -0
- package/dist/tools/get-features.js.map +1 -0
- package/dist/tools/get-scaffold.d.ts +77 -0
- package/dist/tools/get-scaffold.d.ts.map +1 -0
- package/dist/tools/get-scaffold.js +153 -0
- package/dist/tools/get-scaffold.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/utils/docs.d.ts +14 -0
- package/dist/utils/docs.d.ts.map +1 -0
- package/dist/utils/docs.js +64 -0
- package/dist/utils/docs.js.map +1 -0
- package/dist/utils/examples.d.ts +27 -0
- package/dist/utils/examples.d.ts.map +1 -0
- package/dist/utils/examples.js +399 -0
- package/dist/utils/examples.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/paths.d.ts +28 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +40 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/scaffold.d.ts +50 -0
- package/dist/utils/scaffold.d.ts.map +1 -0
- package/dist/utils/scaffold.js +500 -0
- package/dist/utils/scaffold.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +63 -0
- package/templates/.bundled +0 -0
- package/templates/CLAUDE.md +145 -0
- package/templates/docs/API_REFERENCE.md +58 -0
- package/templates/docs/ARCHITECTURE.md +185 -0
- package/templates/docs/CODING_STANDARDS.md +53 -0
- package/templates/docs/COMPONENT_GUIDELINES.md +301 -0
- package/templates/docs/E2E_TESTING.md +116 -0
- package/templates/docs/INTERNATIONALIZATION.md +67 -0
- package/templates/docs/TESTING.md +259 -0
- package/templates/docs/WORKFLOW.md +170 -0
- package/templates/src/App.tsx +42 -0
- package/templates/src/components/layout/Header.tsx +19 -0
- package/templates/src/components/layout/index.ts +1 -0
- package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +104 -0
- package/templates/src/components/shared/ErrorBoundary/index.ts +1 -0
- package/templates/src/components/shared/LanguageSwitcher/LanguageSwitcher.tsx +45 -0
- package/templates/src/components/shared/LanguageSwitcher/index.ts +1 -0
- package/templates/src/components/shared/SEO/SEO.tsx +55 -0
- package/templates/src/components/shared/SEO/index.ts +1 -0
- package/templates/src/components/shared/ThemeToggle/ThemeToggle.tsx +41 -0
- package/templates/src/components/shared/ThemeToggle/index.ts +1 -0
- package/templates/src/components/shared/index.ts +4 -0
- package/templates/src/components/ui/button.tsx +48 -0
- package/templates/src/components/ui/dropdown-menu.tsx +228 -0
- package/templates/src/components/ui/form-error.tsx +95 -0
- package/templates/src/components/ui/loading.tsx +58 -0
- package/templates/src/components/ui/skeleton.tsx +52 -0
- package/templates/src/components/ui/sonner.tsx +34 -0
- package/templates/src/components/ui/spinner.tsx +40 -0
- package/templates/src/components/ui/visually-hidden.tsx +51 -0
- package/templates/src/contexts/mobileContext.tsx +66 -0
- package/templates/src/contexts/queryContext.tsx +28 -0
- package/templates/src/hooks/index.ts +7 -0
- package/templates/src/hooks/useContactForm.ts +33 -0
- package/templates/src/hooks/useExampleQuery.ts +20 -0
- package/templates/src/hooks/useLanguage.ts +23 -0
- package/templates/src/hooks/useMediaQuery.ts +53 -0
- package/templates/src/hooks/useThemeEffect.ts +31 -0
- package/templates/src/hooks/useTouchSizes.ts +16 -0
- package/templates/src/i18n/config.ts +11 -0
- package/templates/src/i18n/detectLanguage.ts +57 -0
- package/templates/src/i18n/index.ts +20 -0
- package/templates/src/i18n/loadCatalog.ts +30 -0
- package/templates/src/index.css +98 -0
- package/templates/src/lib/api.ts +142 -0
- package/templates/src/lib/config.ts +15 -0
- package/templates/src/lib/constants.ts +8 -0
- package/templates/src/lib/env.ts +53 -0
- package/templates/src/lib/format.ts +119 -0
- package/templates/src/lib/index.ts +24 -0
- package/templates/src/lib/routes.ts +11 -0
- package/templates/src/lib/storage.ts +91 -0
- package/templates/src/lib/storageKeys.ts +10 -0
- package/templates/src/lib/utils.ts +6 -0
- package/templates/src/lib/validations.ts +39 -0
- package/templates/src/locales/de.po +65 -0
- package/templates/src/locales/en.po +65 -0
- package/templates/src/locales/es.po +65 -0
- package/templates/src/main.tsx +107 -0
- package/templates/src/mocks/fixtures/index.ts +1 -0
- package/templates/src/mocks/fixtures/todos.ts +40 -0
- package/templates/src/mocks/handlers/index.ts +7 -0
- package/templates/src/mocks/handlers/todos.ts +59 -0
- package/templates/src/mocks/index.ts +3 -0
- package/templates/src/mocks/node.ts +9 -0
- package/templates/src/pages/Home.tsx +27 -0
- package/templates/src/pages/NotFound.tsx +28 -0
- package/templates/src/pages/index.ts +2 -0
- package/templates/src/stores/index.ts +2 -0
- package/templates/src/stores/preferencesStore.ts +85 -0
- package/templates/src/test/index.ts +8 -0
- package/templates/src/test/mocks.ts +17 -0
- package/templates/src/test/providers.tsx +54 -0
- package/templates/src/test-setup.ts +54 -0
- package/templates/src/types/api.ts +31 -0
- package/templates/src/types/index.ts +2 -0
- package/templates/src/types/preferences.ts +5 -0
- package/templates/src/vite-env.d.ts +10 -0
- package/templates/tests/unit/components/ErrorBoundary.test.tsx +193 -0
- package/templates/tests/unit/components/Header.test.tsx +33 -0
- package/templates/tests/unit/components/LanguageSwitcher.test.tsx +40 -0
- package/templates/tests/unit/components/Loading.test.tsx +76 -0
- package/templates/tests/unit/components/SEO.test.tsx +80 -0
- package/templates/tests/unit/components/ThemeToggle.test.tsx +62 -0
- package/templates/tests/unit/contexts/mobileContext.test.tsx +54 -0
- package/templates/tests/unit/hooks/useContactForm.test.ts +60 -0
- package/templates/tests/unit/hooks/useExampleQuery.test.tsx +94 -0
- package/templates/tests/unit/hooks/useLanguage.test.tsx +75 -0
- package/templates/tests/unit/hooks/useMediaQuery.test.ts +57 -0
- package/templates/tests/unit/hooks/useThemeEffect.test.ts +42 -0
- package/templates/tests/unit/i18n/detectLanguage.test.ts +40 -0
- package/templates/tests/unit/i18n/loadCatalog.test.ts +70 -0
- package/templates/tests/unit/lib/api.test.ts +142 -0
- package/templates/tests/unit/lib/format.test.ts +100 -0
- package/templates/tests/unit/lib/storage.test.ts +90 -0
- package/templates/tests/unit/lib/utils.test.ts +19 -0
- package/templates/tests/unit/lib/validations.test.ts +56 -0
- package/templates/tests/unit/stores/preferencesStore.test.ts +75 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# React + TypeScript Component Guidelines
|
|
2
|
+
|
|
3
|
+
A focused blueprint for writing React components. For related patterns, see:
|
|
4
|
+
|
|
5
|
+
- [Coding Standards](./CODING_STANDARDS.md) - TypeScript, state management, hooks
|
|
6
|
+
- [Testing](./TESTING.md) - Unit testing patterns
|
|
7
|
+
- [Internationalization](./INTERNATIONALIZATION.md) - i18n with Lingui
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Component Anatomy
|
|
12
|
+
|
|
13
|
+
Every component follows this structure:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
// 1. Imports (grouped: external → internal → types)
|
|
17
|
+
import { useState } from 'react';
|
|
18
|
+
|
|
19
|
+
import { Button } from '@/components/ui/button';
|
|
20
|
+
import { cn } from '@/lib/utils';
|
|
21
|
+
|
|
22
|
+
// 2. Types/Interfaces
|
|
23
|
+
export interface MyComponentProps {
|
|
24
|
+
title: string;
|
|
25
|
+
count?: number;
|
|
26
|
+
onAction?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 3. Component Implementation
|
|
30
|
+
export function MyComponent({ title, count = 0, onAction }: MyComponentProps) {
|
|
31
|
+
// a. Hooks (all hooks must be called unconditionally)
|
|
32
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
33
|
+
|
|
34
|
+
// b. Derived state / computations
|
|
35
|
+
const displayCount = count > 99 ? '99+' : count;
|
|
36
|
+
|
|
37
|
+
// c. Event handlers
|
|
38
|
+
const handleClick = () => {
|
|
39
|
+
setIsOpen(true);
|
|
40
|
+
onAction?.();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// d. Early returns (only AFTER all hooks)
|
|
44
|
+
if (!title) return null;
|
|
45
|
+
|
|
46
|
+
// e. Render
|
|
47
|
+
return (
|
|
48
|
+
<div>
|
|
49
|
+
<h2>{title}</h2>
|
|
50
|
+
<span>{displayCount}</span>
|
|
51
|
+
<Button onClick={handleClick}>Action</Button>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Props Patterns
|
|
60
|
+
|
|
61
|
+
### Extending HTML Attributes
|
|
62
|
+
|
|
63
|
+
For components wrapping native elements:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { forwardRef } from 'react';
|
|
67
|
+
|
|
68
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
69
|
+
variant?: 'primary' | 'secondary';
|
|
70
|
+
isLoading?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
74
|
+
({ variant = 'primary', isLoading, children, className, ...props }, ref) => {
|
|
75
|
+
return (
|
|
76
|
+
<button ref={ref} className={cn(buttonVariants({ variant }), className)} {...props}>
|
|
77
|
+
{isLoading ? <Spinner /> : children}
|
|
78
|
+
</button>
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
Button.displayName = 'Button';
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Generic Components
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
interface ListProps<T> {
|
|
90
|
+
items: T[];
|
|
91
|
+
renderItem: (item: T) => ReactNode;
|
|
92
|
+
keyExtractor: (item: T) => string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
|
|
96
|
+
return (
|
|
97
|
+
<ul>
|
|
98
|
+
{items.map((item) => (
|
|
99
|
+
<li key={keyExtractor(item)}>{renderItem(item)}</li>
|
|
100
|
+
))}
|
|
101
|
+
</ul>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Component Categories
|
|
109
|
+
|
|
110
|
+
| Category | Location | Naming | Export | Barrel |
|
|
111
|
+
| ------------------ | ---------------------------- | -------------------------- | ------- | ------ |
|
|
112
|
+
| UI Primitives | `components/ui/` | `button.tsx` (lowercase) | Named | No\* |
|
|
113
|
+
| Feature Components | `components/shared/Feature/` | `Feature.tsx` (PascalCase) | Named | Yes |
|
|
114
|
+
| Layout | `components/layout/` | `Header.tsx` (PascalCase) | Named | Yes |
|
|
115
|
+
| Pages | `pages/` | `Home.tsx` (PascalCase) | Default | No |
|
|
116
|
+
|
|
117
|
+
\*UI primitives use direct imports (`@/components/ui/button`) following shadcn/ui conventions.
|
|
118
|
+
|
|
119
|
+
### Feature Component Structure
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
src/components/shared/
|
|
123
|
+
├── ThemeToggle/
|
|
124
|
+
│ ├── ThemeToggle.tsx
|
|
125
|
+
│ └── index.ts # export { ThemeToggle } from './ThemeToggle';
|
|
126
|
+
├── LanguageSwitcher/
|
|
127
|
+
│ └── ...
|
|
128
|
+
└── index.ts # Re-exports all shared components
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## File Organization
|
|
134
|
+
|
|
135
|
+
### Default: Single File
|
|
136
|
+
|
|
137
|
+
Keep types, helpers, and component together in one file. This is the pattern used throughout this codebase—even `dropdown-menu.tsx` at 228 lines remains a single file.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// button.tsx - types and component together
|
|
141
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
142
|
+
variant?: 'primary' | 'secondary';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const buttonVariants = cva(/* ... */);
|
|
146
|
+
|
|
147
|
+
export function Button({ variant, className, ...props }: ButtonProps) {
|
|
148
|
+
return <button className={cn(buttonVariants({ variant }), className)} {...props} />;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### When to Split
|
|
153
|
+
|
|
154
|
+
Split only when you have a **concrete reason**, not preemptively:
|
|
155
|
+
|
|
156
|
+
| Situation | Action |
|
|
157
|
+
| -------------------------------- | --------------------------------- |
|
|
158
|
+
| Types reused by other components | Move to `@/types/` |
|
|
159
|
+
| Helper reused elsewhere | Move to `@/lib/` |
|
|
160
|
+
| File exceeds ~300 lines | Consider splitting by concern |
|
|
161
|
+
| Multiple sub-components | Create folder with separate files |
|
|
162
|
+
|
|
163
|
+
### Splitting Example
|
|
164
|
+
|
|
165
|
+
For a complex component like a data table:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
src/components/shared/DataTable/
|
|
169
|
+
├── DataTable.tsx # Main component
|
|
170
|
+
├── DataTableHeader.tsx # Sub-component
|
|
171
|
+
├── DataTableRow.tsx # Sub-component
|
|
172
|
+
├── columns.tsx # Column configuration
|
|
173
|
+
└── index.ts # export { DataTable } from './DataTable';
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Avoid**: Separate `.types.ts` or `.helpers.ts` files for code used only by that component. Keep related code together.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Styling with CVA
|
|
181
|
+
|
|
182
|
+
Use [Class Variance Authority](https://cva.style/docs) for variant-based styling:
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
186
|
+
import { cn } from '@/lib/utils';
|
|
187
|
+
|
|
188
|
+
const badgeVariants = cva(
|
|
189
|
+
// Base styles
|
|
190
|
+
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
|
|
191
|
+
{
|
|
192
|
+
variants: {
|
|
193
|
+
variant: {
|
|
194
|
+
default: 'bg-primary text-primary-foreground',
|
|
195
|
+
secondary: 'bg-secondary text-secondary-foreground',
|
|
196
|
+
destructive: 'bg-destructive text-destructive-foreground',
|
|
197
|
+
outline: 'border border-input bg-transparent',
|
|
198
|
+
},
|
|
199
|
+
size: {
|
|
200
|
+
sm: 'px-2 py-0.5 text-xs',
|
|
201
|
+
md: 'px-2.5 py-0.5 text-sm',
|
|
202
|
+
lg: 'px-3 py-1 text-base',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
defaultVariants: {
|
|
206
|
+
variant: 'default',
|
|
207
|
+
size: 'md',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
|
|
213
|
+
|
|
214
|
+
export function Badge({ variant, size, className, ...props }: BadgeProps) {
|
|
215
|
+
return <span className={cn(badgeVariants({ variant, size }), className)} {...props} />;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `cn()` Utility
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
cn('px-4 py-2', 'px-6'); // → 'py-2 px-6' (later overrides)
|
|
223
|
+
cn('text-red-500', className); // → allows prop override
|
|
224
|
+
cn(isActive && 'bg-primary'); // → conditional classes
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Loading & Error States
|
|
230
|
+
|
|
231
|
+
Components fetching data should handle all states:
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
export function UserProfile({ userId }: { userId: string }) {
|
|
235
|
+
const { data: user, isLoading, error } = useUserQuery(userId);
|
|
236
|
+
|
|
237
|
+
if (isLoading) {
|
|
238
|
+
return (
|
|
239
|
+
<div className="flex items-center gap-3">
|
|
240
|
+
<Skeleton className="h-10 w-10 rounded-full" />
|
|
241
|
+
<Skeleton className="h-4 w-24" />
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (error || !user) {
|
|
247
|
+
return (
|
|
248
|
+
<div className="text-destructive">
|
|
249
|
+
<Trans comment="Error when user profile fails to load">Failed to load profile</Trans>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div className="flex items-center gap-3">
|
|
256
|
+
<Avatar src={user.avatar} alt={user.name} />
|
|
257
|
+
<span>{user.name}</span>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Accessibility
|
|
266
|
+
|
|
267
|
+
### Required Practices
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
// Icons need labels
|
|
271
|
+
<button aria-label={t({ message: 'Close', comment: 'Close dialog button' })}>
|
|
272
|
+
<XIcon />
|
|
273
|
+
</button>
|
|
274
|
+
|
|
275
|
+
// Use semantic roles
|
|
276
|
+
<header role="banner">...</header>
|
|
277
|
+
<nav role="navigation">...</nav>
|
|
278
|
+
<main role="main">...</main>
|
|
279
|
+
|
|
280
|
+
// Loading states
|
|
281
|
+
<Spinner role="status" aria-label="Loading" />
|
|
282
|
+
|
|
283
|
+
// Interactive elements need focus styles (handled by Tailwind's focus-visible)
|
|
284
|
+
<button className="focus-visible:ring-2 focus-visible:ring-ring">...</button>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Checklist
|
|
290
|
+
|
|
291
|
+
Before submitting a component:
|
|
292
|
+
|
|
293
|
+
- [ ] Props use `interface` with `Props` suffix
|
|
294
|
+
- [ ] Named export (default only for pages)
|
|
295
|
+
- [ ] Imports use `@/` path alias
|
|
296
|
+
- [ ] User-facing text has translator comments
|
|
297
|
+
- [ ] Handles loading/error states for async data
|
|
298
|
+
- [ ] Uses `cn()` for className merging
|
|
299
|
+
- [ ] Accessible (roles, aria-labels, keyboard nav)
|
|
300
|
+
- [ ] Barrel export in `index.ts` (except UI primitives)
|
|
301
|
+
- [ ] Test file in `tests/unit/`
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# E2E Testing Guidelines
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
- **Framework**: Playwright
|
|
6
|
+
- **Test location**: `e2e/tests/`
|
|
7
|
+
- **Utilities**: `e2e/fixtures/`
|
|
8
|
+
|
|
9
|
+
## File Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
e2e/
|
|
13
|
+
├── fixtures/
|
|
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
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Imports
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { expect, test } from '@playwright/test';
|
|
26
|
+
|
|
27
|
+
// For tests that need state clearing
|
|
28
|
+
import { setupPage } from '../fixtures';
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Core Patterns
|
|
32
|
+
|
|
33
|
+
### Simple Page Tests
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
test.describe('Home Page', () => {
|
|
37
|
+
test.beforeEach(async ({ page }) => {
|
|
38
|
+
await page.goto('/');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('displays welcome heading', async ({ page }) => {
|
|
42
|
+
await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Tests Requiring Clean State
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { setupPage } from '../fixtures';
|
|
51
|
+
|
|
52
|
+
test.describe('Theme Toggle', () => {
|
|
53
|
+
test.beforeEach(async ({ page }) => {
|
|
54
|
+
await setupPage(page); // Clears localStorage, reloads
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('persists preference across reload', async ({ page }) => {
|
|
58
|
+
await page.getByRole('button', { name: /dark mode/i }).click();
|
|
59
|
+
await page.reload();
|
|
60
|
+
await expect(page.locator('html')).toHaveClass(/dark/);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Selecting Elements
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// ✅ Good - accessible selectors
|
|
69
|
+
page.getByRole('button', { name: /submit/i });
|
|
70
|
+
page.getByRole('heading', { name: /welcome/i });
|
|
71
|
+
page.getByText('English');
|
|
72
|
+
|
|
73
|
+
// ❌ Avoid
|
|
74
|
+
page.locator('.btn-primary');
|
|
75
|
+
page.locator('div > span.title');
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Waiting for State
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// ✅ Good - wait for visible state change
|
|
82
|
+
await expect(page.getByText('Success')).toBeVisible();
|
|
83
|
+
await expect(page.locator('html')).toHaveClass(/dark/);
|
|
84
|
+
|
|
85
|
+
// ❌ Avoid - arbitrary timeouts
|
|
86
|
+
await page.waitForTimeout(500);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What to Test
|
|
90
|
+
|
|
91
|
+
| Type | Examples |
|
|
92
|
+
| ----------- | --------------------------------- |
|
|
93
|
+
| Navigation | Routes work, 404 handling |
|
|
94
|
+
| User flows | Toggle theme, change language |
|
|
95
|
+
| Persistence | Settings survive reload |
|
|
96
|
+
| Structure | Header present, main content area |
|
|
97
|
+
|
|
98
|
+
## What NOT to Test
|
|
99
|
+
|
|
100
|
+
- CSS class names (implementation detail)
|
|
101
|
+
- Internal component state
|
|
102
|
+
- API response content (use unit tests)
|
|
103
|
+
|
|
104
|
+
## Running Tests
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm run e2e # Run all
|
|
108
|
+
npm run e2e:ui # Interactive UI
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Checklist
|
|
112
|
+
|
|
113
|
+
- [ ] Uses accessible selectors
|
|
114
|
+
- [ ] No arbitrary timeouts
|
|
115
|
+
- [ ] Tests behavior, not implementation
|
|
116
|
+
- [ ] Uses `setupPage` when testing persistence
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Internationalization (i18n)
|
|
2
|
+
|
|
3
|
+
This project uses [Lingui](https://lingui.dev/) for internationalization. **ESLint will warn about untranslated strings.**
|
|
4
|
+
|
|
5
|
+
## Quick Reference
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// Trans component - for JSX content
|
|
9
|
+
<Trans comment="Error shown when upload exceeds 10MB">File too large</Trans>;
|
|
10
|
+
|
|
11
|
+
// t() function - for strings (aria-labels, placeholders, etc.)
|
|
12
|
+
const { t } = useLingui();
|
|
13
|
+
<Button aria-label={t({ message: 'Close', comment: 'Close dialog button' })} />;
|
|
14
|
+
|
|
15
|
+
// msg() - for messages defined outside components
|
|
16
|
+
const labels = {
|
|
17
|
+
save: msg({ message: 'Save', comment: 'Form submit button' }),
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Adding Translations
|
|
22
|
+
|
|
23
|
+
1. Wrap text with `<Trans>` or `t()` and add a `comment`
|
|
24
|
+
2. Run `pnpm i18n:extract` to update PO files
|
|
25
|
+
3. Translate in `src/locales/{locale}.po`
|
|
26
|
+
4. Build compiles translations automatically
|
|
27
|
+
|
|
28
|
+
## Writing Comments
|
|
29
|
+
|
|
30
|
+
Comments help translators understand context. They appear in PO files as `#.` lines.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// Good - explains where and why
|
|
34
|
+
<Trans comment="Error message when file upload exceeds 10MB limit">
|
|
35
|
+
File too large
|
|
36
|
+
</Trans>
|
|
37
|
+
|
|
38
|
+
// Good - explains the variable
|
|
39
|
+
<Trans comment="Badge showing unread count, {count} is 1-99 or 99+">
|
|
40
|
+
{count} new
|
|
41
|
+
</Trans>
|
|
42
|
+
|
|
43
|
+
// Bad - obvious or vague
|
|
44
|
+
<Trans comment="A message">Error</Trans>
|
|
45
|
+
<Trans comment="Title">Welcome</Trans>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Tip:** Use `context` prop when the same word needs different translations:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<Trans context="calendar">Date</Trans>
|
|
52
|
+
<Trans context="romantic">Date</Trans>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## ESLint Rules
|
|
56
|
+
|
|
57
|
+
The `eslint-plugin-lingui` enforces translations:
|
|
58
|
+
|
|
59
|
+
- `no-unlocalized-strings` - Warns about untranslated JSX text
|
|
60
|
+
- `t-call-in-function` - Ensures `t()` is called inside functions
|
|
61
|
+
- `no-trans-inside-trans` - Prevents nested Trans components
|
|
62
|
+
|
|
63
|
+
Excluded from checks: tests, mocks, UI primitives, config files.
|
|
64
|
+
|
|
65
|
+
## Adding a New Locale
|
|
66
|
+
|
|
67
|
+
Edit `lingui.config.js` and add the locale code to the `locales` array.
|