@idealyst/cli 1.0.90 → 1.0.92
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/dist/generators/init.js +8 -13
- package/dist/generators/init.js.map +1 -1
- package/dist/generators/utils.js +3 -7
- package/dist/generators/utils.js.map +1 -1
- package/dist/template/.devcontainer/gitignore.template +2 -0
- package/dist/template/gitignore.template +56 -0
- package/dist/template/mcp.json.template +8 -0
- package/dist/template/packages/api/gitignore.template +35 -0
- package/dist/template/packages/database/gitignore.template +41 -0
- package/dist/template/packages/mobile/babel.config.js +1 -2
- package/dist/template/packages/mobile/gitignore.template +73 -0
- package/dist/template/packages/mobile/package.json +10 -2
- package/dist/template/packages/shared/gitignore.template +35 -0
- package/dist/template/packages/web/gitignore.template +35 -0
- package/dist/template/yarnrc.yml.template +4 -0
- package/package.json +2 -2
- package/template/.devcontainer/Dockerfile +26 -0
- package/template/.devcontainer/devcontainer.json +113 -0
- package/template/.devcontainer/docker-compose.yml +59 -0
- package/template/.devcontainer/figma-mcp.sh +32 -0
- package/template/.devcontainer/setup.sh +45 -0
- package/template/.dockerignore +151 -0
- package/template/.env.example +36 -0
- package/template/.env.production +56 -0
- package/template/DOCKER.md +0 -0
- package/template/Dockerfile +111 -0
- package/template/README.md +233 -0
- package/template/docker/nginx/prod.conf +238 -0
- package/template/docker/nginx.conf +131 -0
- package/template/docker/postgres/init.sql +41 -0
- package/template/docker/prometheus/prometheus.yml +52 -0
- package/template/docker-compose.prod.yml +146 -0
- package/template/docker-compose.yml +143 -0
- package/template/jest.config.js +20 -0
- package/template/package.json +45 -0
- package/template/packages/api/.env.example +6 -0
- package/template/packages/api/README.md +274 -0
- package/template/packages/api/__tests__/api.test.ts +26 -0
- package/template/packages/api/jest.config.js +23 -0
- package/template/packages/api/jest.setup.js +9 -0
- package/template/packages/api/package.json +56 -0
- package/template/packages/api/src/context.ts +19 -0
- package/template/packages/api/src/controllers/TestController.ts +0 -0
- package/template/packages/api/src/index.ts +9 -0
- package/template/packages/api/src/lib/crud.ts +150 -0
- package/template/packages/api/src/lib/database.ts +23 -0
- package/template/packages/api/src/router/index.ts +163 -0
- package/template/packages/api/src/routers/test.ts +59 -0
- package/template/packages/api/src/routers/user.example.ts +83 -0
- package/template/packages/api/src/server.ts +50 -0
- package/template/packages/api/src/trpc.ts +28 -0
- package/template/packages/api/tsconfig.json +43 -0
- package/template/packages/database/README.md +162 -0
- package/template/packages/database/package.json +49 -0
- package/template/packages/database/prisma/seed.ts +64 -0
- package/template/packages/database/schema.prisma +107 -0
- package/template/packages/database/src/index.ts +15 -0
- package/template/packages/database/src/validators.ts +10 -0
- package/template/packages/database/tsconfig.json +18 -0
- package/template/packages/mobile/README.md +86 -0
- package/template/packages/mobile/__tests__/App.test.tsx +156 -0
- package/template/packages/mobile/__tests__/components.test.tsx +300 -0
- package/template/packages/mobile/app.json +5 -0
- package/template/packages/mobile/babel.config.js +10 -0
- package/template/packages/mobile/index.js +6 -0
- package/template/packages/mobile/jest.config.js +21 -0
- package/template/packages/mobile/jest.setup.js +12 -0
- package/template/packages/mobile/metro.config.js +27 -0
- package/template/packages/mobile/package.json +60 -0
- package/template/packages/mobile/src/utils/trpc.ts +7 -0
- package/template/packages/mobile/tsconfig.json +28 -0
- package/template/packages/shared/README.md +135 -0
- package/template/packages/shared/__tests__/shared.test.ts +51 -0
- package/template/packages/shared/jest.config.js +22 -0
- package/template/packages/shared/package.json +62 -0
- package/template/packages/shared/src/components/App.tsx +46 -0
- package/template/packages/shared/src/components/HelloWorld.tsx +304 -0
- package/template/packages/shared/src/components/index.ts +1 -0
- package/template/packages/shared/src/index.ts +14 -0
- package/template/packages/shared/src/navigation/AppRouter.tsx +565 -0
- package/template/packages/shared/src/trpc/client.ts +44 -0
- package/template/packages/shared/tsconfig.json +22 -0
- package/template/packages/web/README.md +131 -0
- package/template/packages/web/__tests__/App.test.tsx +342 -0
- package/template/packages/web/__tests__/components.test.tsx +564 -0
- package/template/packages/web/index.html +13 -0
- package/template/packages/web/jest.config.js +27 -0
- package/template/packages/web/jest.setup.js +24 -0
- package/template/packages/web/package.json +69 -0
- package/template/packages/web/src/components/TestDemo.tsx +164 -0
- package/template/packages/web/src/main.tsx +25 -0
- package/template/packages/web/src/utils/trpc.ts +7 -0
- package/template/packages/web/tsconfig.json +26 -0
- package/template/packages/web/vite.config.ts +98 -0
- package/template/setup.sh +30 -0
- package/template/tsconfig.json +31 -0
- package/dist/template/packages/mobile/src/App-with-trpc.tsx +0 -30
- package/dist/template/packages/web/src/App-with-trpc.tsx +0 -32
- /package/dist/template/{.dockerignore → dockerignore.template} +0 -0
- /package/dist/template/{.env.example → env.example.template} +0 -0
- /package/dist/template/packages/api/{.env.example → env.example.template} +0 -0
- /package/{dist/template/packages/mobile/src/App-with-trpc-and-shared.tsx → template/packages/mobile/src/App.tsx} +0 -0
- /package/{dist/template/packages/web/src/App-with-trpc-and-shared.tsx → template/packages/web/src/App.tsx} +0 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example component tests for React Web
|
|
3
|
+
* Demonstrates comprehensive testing patterns with React Testing Library
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
|
|
10
|
+
describe('React Web Component Testing Examples', () => {
|
|
11
|
+
// Example 1: Simple Display Component
|
|
12
|
+
const WelcomeMessage = ({
|
|
13
|
+
name,
|
|
14
|
+
showGreeting = true
|
|
15
|
+
}: {
|
|
16
|
+
name: string;
|
|
17
|
+
showGreeting?: boolean;
|
|
18
|
+
}) => (
|
|
19
|
+
<div data-testid="welcome-message">
|
|
20
|
+
{showGreeting && <h1>Welcome, {name}!</h1>}
|
|
21
|
+
<p>Thank you for visiting our site.</p>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
describe('WelcomeMessage Component', () => {
|
|
26
|
+
it('renders welcome message with name', () => {
|
|
27
|
+
render(<WelcomeMessage name="John" />);
|
|
28
|
+
|
|
29
|
+
expect(screen.getByText('Welcome, John!')).toBeInTheDocument();
|
|
30
|
+
expect(screen.getByText('Thank you for visiting our site.')).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('hides greeting when showGreeting is false', () => {
|
|
34
|
+
render(<WelcomeMessage name="John" showGreeting={false} />);
|
|
35
|
+
|
|
36
|
+
expect(screen.queryByText('Welcome, John!')).not.toBeInTheDocument();
|
|
37
|
+
expect(screen.getByText('Thank you for visiting our site.')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Example 2: Interactive Button Component
|
|
42
|
+
const ActionButton = ({
|
|
43
|
+
children,
|
|
44
|
+
onClick,
|
|
45
|
+
variant = 'primary',
|
|
46
|
+
disabled = false,
|
|
47
|
+
loading = false
|
|
48
|
+
}: {
|
|
49
|
+
children: React.ReactNode;
|
|
50
|
+
onClick: () => void;
|
|
51
|
+
variant?: 'primary' | 'secondary' | 'danger';
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
loading?: boolean;
|
|
54
|
+
}) => (
|
|
55
|
+
<button
|
|
56
|
+
data-testid="action-button"
|
|
57
|
+
onClick={onClick}
|
|
58
|
+
disabled={disabled || loading}
|
|
59
|
+
className={`btn btn-${variant} ${loading ? 'loading' : ''}`}
|
|
60
|
+
aria-label={loading ? 'Loading...' : undefined}
|
|
61
|
+
>
|
|
62
|
+
{loading ? 'Loading...' : children}
|
|
63
|
+
</button>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
describe('ActionButton Component', () => {
|
|
67
|
+
it('renders button with correct text', () => {
|
|
68
|
+
const mockClick = jest.fn();
|
|
69
|
+
render(<ActionButton onClick={mockClick}>Click me</ActionButton>);
|
|
70
|
+
|
|
71
|
+
const button = screen.getByTestId('action-button');
|
|
72
|
+
expect(button).toHaveTextContent('Click me');
|
|
73
|
+
expect(button).toHaveClass('btn', 'btn-primary');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('calls onClick when clicked', async () => {
|
|
77
|
+
const user = userEvent.setup();
|
|
78
|
+
const mockClick = jest.fn();
|
|
79
|
+
|
|
80
|
+
render(<ActionButton onClick={mockClick}>Click me</ActionButton>);
|
|
81
|
+
|
|
82
|
+
await user.click(screen.getByTestId('action-button'));
|
|
83
|
+
expect(mockClick).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('applies correct variant class', () => {
|
|
87
|
+
const mockClick = jest.fn();
|
|
88
|
+
render(<ActionButton onClick={mockClick} variant="danger">Delete</ActionButton>);
|
|
89
|
+
|
|
90
|
+
expect(screen.getByTestId('action-button')).toHaveClass('btn-danger');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('shows loading state', () => {
|
|
94
|
+
const mockClick = jest.fn();
|
|
95
|
+
render(<ActionButton onClick={mockClick} loading>Submit</ActionButton>);
|
|
96
|
+
|
|
97
|
+
const button = screen.getByTestId('action-button');
|
|
98
|
+
expect(button).toHaveTextContent('Loading...');
|
|
99
|
+
expect(button).toBeDisabled();
|
|
100
|
+
expect(button).toHaveClass('loading');
|
|
101
|
+
expect(button).toHaveAttribute('aria-label', 'Loading...');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('is disabled when disabled prop is true', () => {
|
|
105
|
+
const mockClick = jest.fn();
|
|
106
|
+
render(<ActionButton onClick={mockClick} disabled>Disabled</ActionButton>);
|
|
107
|
+
|
|
108
|
+
expect(screen.getByTestId('action-button')).toBeDisabled();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Example 3: Form Component with Validation
|
|
113
|
+
const ContactForm = ({
|
|
114
|
+
onSubmit
|
|
115
|
+
}: {
|
|
116
|
+
onSubmit: (data: { name: string; email: string; message: string }) => void;
|
|
117
|
+
}) => {
|
|
118
|
+
const [formData, setFormData] = React.useState({
|
|
119
|
+
name: '',
|
|
120
|
+
email: '',
|
|
121
|
+
message: ''
|
|
122
|
+
});
|
|
123
|
+
const [errors, setErrors] = React.useState<Record<string, string>>({});
|
|
124
|
+
|
|
125
|
+
const validateForm = () => {
|
|
126
|
+
const newErrors: Record<string, string> = {};
|
|
127
|
+
|
|
128
|
+
if (!formData.name.trim()) {
|
|
129
|
+
newErrors.name = 'Name is required';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!formData.email.trim()) {
|
|
133
|
+
newErrors.email = 'Email is required';
|
|
134
|
+
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
|
135
|
+
newErrors.email = 'Email is invalid';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!formData.message.trim()) {
|
|
139
|
+
newErrors.message = 'Message is required';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setErrors(newErrors);
|
|
143
|
+
return Object.keys(newErrors).length === 0;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
if (validateForm()) {
|
|
149
|
+
onSubmit(formData);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const handleInputChange = (field: string, value: string) => {
|
|
154
|
+
setFormData(prev => ({ ...prev, [field]: value }));
|
|
155
|
+
if (errors[field]) {
|
|
156
|
+
setErrors(prev => ({ ...prev, [field]: '' }));
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<form data-testid="contact-form" onSubmit={handleSubmit}>
|
|
162
|
+
<div>
|
|
163
|
+
<label htmlFor="name">Name:</label>
|
|
164
|
+
<input
|
|
165
|
+
id="name"
|
|
166
|
+
data-testid="name-input"
|
|
167
|
+
type="text"
|
|
168
|
+
value={formData.name}
|
|
169
|
+
onChange={(e) => handleInputChange('name', e.target.value)}
|
|
170
|
+
aria-describedby={errors.name ? 'name-error' : undefined}
|
|
171
|
+
/>
|
|
172
|
+
{errors.name && (
|
|
173
|
+
<div id="name-error" data-testid="name-error" role="alert">
|
|
174
|
+
{errors.name}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div>
|
|
180
|
+
<label htmlFor="email">Email:</label>
|
|
181
|
+
<input
|
|
182
|
+
id="email"
|
|
183
|
+
data-testid="email-input"
|
|
184
|
+
type="email"
|
|
185
|
+
value={formData.email}
|
|
186
|
+
onChange={(e) => handleInputChange('email', e.target.value)}
|
|
187
|
+
aria-describedby={errors.email ? 'email-error' : undefined}
|
|
188
|
+
/>
|
|
189
|
+
{errors.email && (
|
|
190
|
+
<div id="email-error" data-testid="email-error" role="alert">
|
|
191
|
+
{errors.email}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div>
|
|
197
|
+
<label htmlFor="message">Message:</label>
|
|
198
|
+
<textarea
|
|
199
|
+
id="message"
|
|
200
|
+
data-testid="message-input"
|
|
201
|
+
value={formData.message}
|
|
202
|
+
onChange={(e) => handleInputChange('message', e.target.value)}
|
|
203
|
+
aria-describedby={errors.message ? 'message-error' : undefined}
|
|
204
|
+
/>
|
|
205
|
+
{errors.message && (
|
|
206
|
+
<div id="message-error" data-testid="message-error" role="alert">
|
|
207
|
+
{errors.message}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<button data-testid="submit-button" type="submit">
|
|
213
|
+
Send Message
|
|
214
|
+
</button>
|
|
215
|
+
</form>
|
|
216
|
+
);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
describe('ContactForm Component', () => {
|
|
220
|
+
it('renders all form fields', () => {
|
|
221
|
+
const mockSubmit = jest.fn();
|
|
222
|
+
render(<ContactForm onSubmit={mockSubmit} />);
|
|
223
|
+
|
|
224
|
+
expect(screen.getByLabelText('Name:')).toBeInTheDocument();
|
|
225
|
+
expect(screen.getByLabelText('Email:')).toBeInTheDocument();
|
|
226
|
+
expect(screen.getByLabelText('Message:')).toBeInTheDocument();
|
|
227
|
+
expect(screen.getByRole('button', { name: 'Send Message' })).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('submits form with valid data', async () => {
|
|
231
|
+
const user = userEvent.setup();
|
|
232
|
+
const mockSubmit = jest.fn();
|
|
233
|
+
|
|
234
|
+
render(<ContactForm onSubmit={mockSubmit} />);
|
|
235
|
+
|
|
236
|
+
await user.type(screen.getByLabelText('Name:'), 'John Doe');
|
|
237
|
+
await user.type(screen.getByLabelText('Email:'), 'john@example.com');
|
|
238
|
+
await user.type(screen.getByLabelText('Message:'), 'Hello there!');
|
|
239
|
+
|
|
240
|
+
await user.click(screen.getByRole('button', { name: 'Send Message' }));
|
|
241
|
+
|
|
242
|
+
expect(mockSubmit).toHaveBeenCalledWith({
|
|
243
|
+
name: 'John Doe',
|
|
244
|
+
email: 'john@example.com',
|
|
245
|
+
message: 'Hello there!'
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('shows validation errors for empty fields', async () => {
|
|
250
|
+
const user = userEvent.setup();
|
|
251
|
+
const mockSubmit = jest.fn();
|
|
252
|
+
|
|
253
|
+
render(<ContactForm onSubmit={mockSubmit} />);
|
|
254
|
+
|
|
255
|
+
await user.click(screen.getByRole('button', { name: 'Send Message' }));
|
|
256
|
+
|
|
257
|
+
expect(screen.getByTestId('name-error')).toHaveTextContent('Name is required');
|
|
258
|
+
expect(screen.getByTestId('email-error')).toHaveTextContent('Email is required');
|
|
259
|
+
expect(screen.getByTestId('message-error')).toHaveTextContent('Message is required');
|
|
260
|
+
expect(mockSubmit).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('shows email validation error for invalid email', async () => {
|
|
264
|
+
const user = userEvent.setup();
|
|
265
|
+
const mockSubmit = jest.fn();
|
|
266
|
+
|
|
267
|
+
render(<ContactForm onSubmit={mockSubmit} />);
|
|
268
|
+
|
|
269
|
+
await user.type(screen.getByLabelText('Email:'), 'invalid-email');
|
|
270
|
+
await user.click(screen.getByRole('button', { name: 'Send Message' }));
|
|
271
|
+
|
|
272
|
+
expect(screen.getByTestId('email-error')).toHaveTextContent('Email is invalid');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('clears errors when user starts typing', async () => {
|
|
276
|
+
const user = userEvent.setup();
|
|
277
|
+
const mockSubmit = jest.fn();
|
|
278
|
+
|
|
279
|
+
render(<ContactForm onSubmit={mockSubmit} />);
|
|
280
|
+
|
|
281
|
+
// Trigger validation errors
|
|
282
|
+
await user.click(screen.getByRole('button', { name: 'Send Message' }));
|
|
283
|
+
expect(screen.getByTestId('name-error')).toBeInTheDocument();
|
|
284
|
+
|
|
285
|
+
// Start typing in name field
|
|
286
|
+
await user.type(screen.getByLabelText('Name:'), 'J');
|
|
287
|
+
|
|
288
|
+
// Error should be cleared
|
|
289
|
+
expect(screen.queryByTestId('name-error')).not.toBeInTheDocument();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Example 4: Data Fetching Component
|
|
294
|
+
const UserProfile = ({ userId }: { userId: string }) => {
|
|
295
|
+
const [user, setUser] = React.useState<{ id: string; name: string; email: string } | null>(null);
|
|
296
|
+
const [loading, setLoading] = React.useState(true);
|
|
297
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
298
|
+
|
|
299
|
+
const fetchUser = React.useCallback(async () => {
|
|
300
|
+
setLoading(true);
|
|
301
|
+
setError(null);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
// Simulate API call
|
|
305
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
306
|
+
|
|
307
|
+
if (userId === 'invalid') {
|
|
308
|
+
throw new Error('User not found');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
setUser({
|
|
312
|
+
id: userId,
|
|
313
|
+
name: `User ${userId}`,
|
|
314
|
+
email: `user${userId}@example.com`
|
|
315
|
+
});
|
|
316
|
+
} catch (err) {
|
|
317
|
+
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
318
|
+
} finally {
|
|
319
|
+
setLoading(false);
|
|
320
|
+
}
|
|
321
|
+
}, [userId]);
|
|
322
|
+
|
|
323
|
+
React.useEffect(() => {
|
|
324
|
+
fetchUser();
|
|
325
|
+
}, [fetchUser]);
|
|
326
|
+
|
|
327
|
+
if (loading) {
|
|
328
|
+
return <div data-testid="loading">Loading user...</div>;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (error) {
|
|
332
|
+
return (
|
|
333
|
+
<div data-testid="error">
|
|
334
|
+
<p>Error: {error}</p>
|
|
335
|
+
<button data-testid="retry-button" onClick={fetchUser}>
|
|
336
|
+
Retry
|
|
337
|
+
</button>
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!user) {
|
|
343
|
+
return <div data-testid="no-user">No user found</div>;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div data-testid="user-profile">
|
|
348
|
+
<h2>{user.name}</h2>
|
|
349
|
+
<p>Email: {user.email}</p>
|
|
350
|
+
<p>ID: {user.id}</p>
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
describe('UserProfile Component', () => {
|
|
356
|
+
it('shows loading state initially', () => {
|
|
357
|
+
render(<UserProfile userId="123" />);
|
|
358
|
+
|
|
359
|
+
expect(screen.getByTestId('loading')).toBeInTheDocument();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('shows user data after successful fetch', async () => {
|
|
363
|
+
render(<UserProfile userId="123" />);
|
|
364
|
+
|
|
365
|
+
await waitFor(() => {
|
|
366
|
+
expect(screen.getByTestId('user-profile')).toBeInTheDocument();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(screen.getByText('User 123')).toBeInTheDocument();
|
|
370
|
+
expect(screen.getByText('Email: user123@example.com')).toBeInTheDocument();
|
|
371
|
+
expect(screen.getByText('ID: 123')).toBeInTheDocument();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('shows error state when fetch fails', async () => {
|
|
375
|
+
render(<UserProfile userId="invalid" />);
|
|
376
|
+
|
|
377
|
+
await waitFor(() => {
|
|
378
|
+
expect(screen.getByTestId('error')).toBeInTheDocument();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(screen.getByText('Error: User not found')).toBeInTheDocument();
|
|
382
|
+
expect(screen.getByTestId('retry-button')).toBeInTheDocument();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('retries fetch when retry button is clicked', async () => {
|
|
386
|
+
const user = userEvent.setup();
|
|
387
|
+
const { rerender } = render(<UserProfile userId="invalid" />);
|
|
388
|
+
|
|
389
|
+
// Wait for error state
|
|
390
|
+
await waitFor(() => {
|
|
391
|
+
expect(screen.getByTestId('error')).toBeInTheDocument();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Change userId to valid and click retry
|
|
395
|
+
rerender(<UserProfile userId="456" />);
|
|
396
|
+
await user.click(screen.getByTestId('retry-button'));
|
|
397
|
+
|
|
398
|
+
// Should show loading then success
|
|
399
|
+
expect(screen.getByTestId('loading')).toBeInTheDocument();
|
|
400
|
+
|
|
401
|
+
await waitFor(() => {
|
|
402
|
+
expect(screen.getByTestId('user-profile')).toBeInTheDocument();
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Example 5: Modal Component
|
|
408
|
+
const Modal = ({
|
|
409
|
+
isOpen,
|
|
410
|
+
onClose,
|
|
411
|
+
title,
|
|
412
|
+
children
|
|
413
|
+
}: {
|
|
414
|
+
isOpen: boolean;
|
|
415
|
+
onClose: () => void;
|
|
416
|
+
title: string;
|
|
417
|
+
children: React.ReactNode;
|
|
418
|
+
}) => {
|
|
419
|
+
React.useEffect(() => {
|
|
420
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
421
|
+
if (e.key === 'Escape') {
|
|
422
|
+
onClose();
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
if (isOpen) {
|
|
427
|
+
document.addEventListener('keydown', handleEscape);
|
|
428
|
+
document.body.style.overflow = 'hidden';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return () => {
|
|
432
|
+
document.removeEventListener('keydown', handleEscape);
|
|
433
|
+
document.body.style.overflow = '';
|
|
434
|
+
};
|
|
435
|
+
}, [isOpen, onClose]);
|
|
436
|
+
|
|
437
|
+
if (!isOpen) return null;
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<div
|
|
441
|
+
data-testid="modal-overlay"
|
|
442
|
+
className="modal-overlay"
|
|
443
|
+
onClick={onClose}
|
|
444
|
+
>
|
|
445
|
+
<div
|
|
446
|
+
data-testid="modal-content"
|
|
447
|
+
className="modal-content"
|
|
448
|
+
onClick={(e) => e.stopPropagation()}
|
|
449
|
+
role="dialog"
|
|
450
|
+
aria-labelledby="modal-title"
|
|
451
|
+
>
|
|
452
|
+
<header className="modal-header">
|
|
453
|
+
<h2 id="modal-title">{title}</h2>
|
|
454
|
+
<button
|
|
455
|
+
data-testid="close-button"
|
|
456
|
+
onClick={onClose}
|
|
457
|
+
aria-label="Close modal"
|
|
458
|
+
>
|
|
459
|
+
×
|
|
460
|
+
</button>
|
|
461
|
+
</header>
|
|
462
|
+
<div className="modal-body">
|
|
463
|
+
{children}
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
);
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
describe('Modal Component', () => {
|
|
471
|
+
it('does not render when isOpen is false', () => {
|
|
472
|
+
render(
|
|
473
|
+
<Modal isOpen={false} onClose={jest.fn()} title="Test Modal">
|
|
474
|
+
<p>Modal content</p>
|
|
475
|
+
</Modal>
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
expect(screen.queryByTestId('modal-overlay')).not.toBeInTheDocument();
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('renders when isOpen is true', () => {
|
|
482
|
+
render(
|
|
483
|
+
<Modal isOpen={true} onClose={jest.fn()} title="Test Modal">
|
|
484
|
+
<p>Modal content</p>
|
|
485
|
+
</Modal>
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
expect(screen.getByTestId('modal-overlay')).toBeInTheDocument();
|
|
489
|
+
expect(screen.getByText('Test Modal')).toBeInTheDocument();
|
|
490
|
+
expect(screen.getByText('Modal content')).toBeInTheDocument();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('calls onClose when close button is clicked', async () => {
|
|
494
|
+
const user = userEvent.setup();
|
|
495
|
+
const mockClose = jest.fn();
|
|
496
|
+
|
|
497
|
+
render(
|
|
498
|
+
<Modal isOpen={true} onClose={mockClose} title="Test Modal">
|
|
499
|
+
<p>Modal content</p>
|
|
500
|
+
</Modal>
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
await user.click(screen.getByTestId('close-button'));
|
|
504
|
+
expect(mockClose).toHaveBeenCalledTimes(1);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('calls onClose when overlay is clicked', async () => {
|
|
508
|
+
const user = userEvent.setup();
|
|
509
|
+
const mockClose = jest.fn();
|
|
510
|
+
|
|
511
|
+
render(
|
|
512
|
+
<Modal isOpen={true} onClose={mockClose} title="Test Modal">
|
|
513
|
+
<p>Modal content</p>
|
|
514
|
+
</Modal>
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
await user.click(screen.getByTestId('modal-overlay'));
|
|
518
|
+
expect(mockClose).toHaveBeenCalledTimes(1);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('does not close when modal content is clicked', async () => {
|
|
522
|
+
const user = userEvent.setup();
|
|
523
|
+
const mockClose = jest.fn();
|
|
524
|
+
|
|
525
|
+
render(
|
|
526
|
+
<Modal isOpen={true} onClose={mockClose} title="Test Modal">
|
|
527
|
+
<p>Modal content</p>
|
|
528
|
+
</Modal>
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
await user.click(screen.getByTestId('modal-content'));
|
|
532
|
+
expect(mockClose).not.toHaveBeenCalled();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('closes when Escape key is pressed', async () => {
|
|
536
|
+
const user = userEvent.setup();
|
|
537
|
+
const mockClose = jest.fn();
|
|
538
|
+
|
|
539
|
+
render(
|
|
540
|
+
<Modal isOpen={true} onClose={mockClose} title="Test Modal">
|
|
541
|
+
<p>Modal content</p>
|
|
542
|
+
</Modal>
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
await user.keyboard('{Escape}');
|
|
546
|
+
expect(mockClose).toHaveBeenCalledTimes(1);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('has proper accessibility attributes', () => {
|
|
550
|
+
render(
|
|
551
|
+
<Modal isOpen={true} onClose={jest.fn()} title="Test Modal">
|
|
552
|
+
<p>Modal content</p>
|
|
553
|
+
</Modal>
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
const modalContent = screen.getByTestId('modal-content');
|
|
557
|
+
expect(modalContent).toHaveAttribute('role', 'dialog');
|
|
558
|
+
expect(modalContent).toHaveAttribute('aria-labelledby', 'modal-title');
|
|
559
|
+
|
|
560
|
+
const closeButton = screen.getByTestId('close-button');
|
|
561
|
+
expect(closeButton).toHaveAttribute('aria-label', 'Close modal');
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{projectName}}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'jsdom',
|
|
5
|
+
roots: ['<rootDir>/src', '<rootDir>/__tests__'],
|
|
6
|
+
testMatch: [
|
|
7
|
+
'**/__tests__/**/*.{ts,tsx,js}',
|
|
8
|
+
'**/*.{test,spec}.{ts,tsx,js}'
|
|
9
|
+
],
|
|
10
|
+
transform: {
|
|
11
|
+
'^.+\\.tsx?$': 'ts-jest',
|
|
12
|
+
},
|
|
13
|
+
moduleNameMapping: {
|
|
14
|
+
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
|
15
|
+
},
|
|
16
|
+
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
|
17
|
+
collectCoverageFrom: [
|
|
18
|
+
'src/**/*.{ts,tsx}',
|
|
19
|
+
'!src/**/*.d.ts',
|
|
20
|
+
'!src/**/index.ts',
|
|
21
|
+
'!src/main.tsx',
|
|
22
|
+
],
|
|
23
|
+
coverageDirectory: 'coverage',
|
|
24
|
+
coverageReporters: ['text', 'lcov'],
|
|
25
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
|
26
|
+
testTimeout: 10000,
|
|
27
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Global test setup for Web/React tests
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
|
|
4
|
+
// Mock window.matchMedia for tests
|
|
5
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
6
|
+
writable: true,
|
|
7
|
+
value: jest.fn().mockImplementation(query => ({
|
|
8
|
+
matches: false,
|
|
9
|
+
media: query,
|
|
10
|
+
onchange: null,
|
|
11
|
+
addListener: jest.fn(), // deprecated
|
|
12
|
+
removeListener: jest.fn(), // deprecated
|
|
13
|
+
addEventListener: jest.fn(),
|
|
14
|
+
removeEventListener: jest.fn(),
|
|
15
|
+
dispatchEvent: jest.fn(),
|
|
16
|
+
})),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Mock ResizeObserver
|
|
20
|
+
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
|
21
|
+
observe: jest.fn(),
|
|
22
|
+
unobserve: jest.fn(),
|
|
23
|
+
disconnect: jest.fn(),
|
|
24
|
+
}));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{workspaceScope}}/web",
|
|
3
|
+
"version": "{{version}}",
|
|
4
|
+
"description": "{{description}}",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "tsc && vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"test:watch": "jest --watch",
|
|
13
|
+
"test:coverage": "jest --coverage"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@idealyst/components": "^{{idealystVersion}}",
|
|
17
|
+
"@idealyst/navigation": "^{{idealystVersion}}",
|
|
18
|
+
"@idealyst/theme": "^{{idealystVersion}}",
|
|
19
|
+
"@{{workspaceScope}}/shared": "workspace:*",
|
|
20
|
+
"@{{workspaceScope}}/database": "workspace:*",
|
|
21
|
+
"@{{workspaceScope}}/api": "workspace:*",
|
|
22
|
+
"@mdi/js": "^7.4.47",
|
|
23
|
+
"@mdi/react": "^1.6.1",
|
|
24
|
+
"@react-native/normalize-colors": "^0.80.1",
|
|
25
|
+
"@tanstack/react-query": "^5.83.0",
|
|
26
|
+
"@trpc/client": "^11.5.1",
|
|
27
|
+
"@trpc/react-query": "^11.5.1",
|
|
28
|
+
"@trpc/server": "^11.5.1",
|
|
29
|
+
"@types/react-router-dom": "^5.3.3",
|
|
30
|
+
"compression": "^1.7.4",
|
|
31
|
+
"express": "^4.18.2",
|
|
32
|
+
"react": "^19.1.0",
|
|
33
|
+
"react-dom": "^19.1.0",
|
|
34
|
+
"react-native": "^0.80.1",
|
|
35
|
+
"react-native-edge-to-edge": "^1.6.2",
|
|
36
|
+
"react-native-nitro-modules": "0.30.0",
|
|
37
|
+
"react-native-unistyles": "^3.0.10",
|
|
38
|
+
"react-native-web": "^0.20.0",
|
|
39
|
+
"react-router": "^7.6.3",
|
|
40
|
+
"react-router-dom": "^7.6.3",
|
|
41
|
+
"sirv": "^2.0.4"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@babel/core": "^7.28.0",
|
|
45
|
+
"@babel/preset-env": "^7.28.0",
|
|
46
|
+
"@babel/preset-react": "^7.27.1",
|
|
47
|
+
"@babel/preset-typescript": "^7.27.1",
|
|
48
|
+
"@testing-library/jest-dom": "^6.4.2",
|
|
49
|
+
"@testing-library/react": "^14.2.1",
|
|
50
|
+
"@testing-library/user-event": "^14.5.2",
|
|
51
|
+
"@types/compression": "^1.7.5",
|
|
52
|
+
"@types/express": "^4.17.21",
|
|
53
|
+
"@types/jest": "^29.5.12",
|
|
54
|
+
"@types/react": "^19.1.0",
|
|
55
|
+
"@types/react-dom": "^19.1.0",
|
|
56
|
+
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
|
57
|
+
"@typescript-eslint/parser": "^7.2.0",
|
|
58
|
+
"@vitejs/plugin-react": "^4.6.0",
|
|
59
|
+
"eslint": "^8.57.0",
|
|
60
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
61
|
+
"eslint-plugin-react-refresh": "^0.4.6",
|
|
62
|
+
"jest": "^29.7.0",
|
|
63
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
64
|
+
"ts-jest": "^29.1.2",
|
|
65
|
+
"typescript": "^5.2.2",
|
|
66
|
+
"vite": "^5.2.0",
|
|
67
|
+
"vite-plugin-babel": "^1.3.2"
|
|
68
|
+
}
|
|
69
|
+
}
|