1
1
---
2
2
description: Frontend accessibility guidelines and testing patterns
3
3
globs: "**/*"
4
- alwaysApply: false
4
+ alwaysApply: true
5
5
---
6
6
7
7
# Accessibility & Testing Guidelines
8
8
9
9
## Accessibility Guidelines
10
10
11
11
### Semantic HTML
12
+
12
13
```typescript
13
14
// ✅ Use semantic HTML elements
14
15
<header className="comet-header">
@@ -38,6 +39,7 @@ alwaysApply: false
38
39
```
39
40
40
41
### ARIA Labels and Descriptions
42
+
41
43
```typescript
42
44
// ✅ Always provide aria-label for icon buttons
43
45
<Button variant="ghost" size="icon" aria-label="Delete item">
@@ -61,7 +63,7 @@ alwaysApply: false
61
63
</div>
62
64
63
65
// ✅ Use aria-expanded for collapsible content
64
- <Button
66
+ <Button
65
67
aria-expanded={isOpen}
66
68
aria-controls="dropdown-menu"
67
69
onClick={() => setIsOpen(!isOpen)}
@@ -74,6 +76,7 @@ alwaysApply: false
74
76
```
75
77
76
78
### Keyboard Navigation
79
+
77
80
```typescript
78
81
// ✅ Ensure keyboard navigation works
79
82
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
@@ -98,6 +101,7 @@ const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
98
101
```
99
102
100
103
### Focus Management
104
+
101
105
```typescript
102
106
// ✅ Provide visible focus indicators
103
107
.focus-visible:focus {
@@ -122,23 +126,24 @@ import { FocusTrap } from '@radix-ui/react-focus-trap';
122
126
```
123
127
124
128
### Heading Hierarchy
129
+
125
130
```typescript
126
131
// ✅ Use proper heading hierarchy
127
132
<div className="page">
128
133
<h1>Dashboard</h1>
129
-
134
+
130
135
<section>
131
136
<h2>Recent Projects</h2>
132
-
137
+
133
138
<article>
134
139
<h3>Project Name</h3>
135
140
<p>Project description...</p>
136
141
</article>
137
142
</section>
138
-
143
+
139
144
<section>
140
145
<h2>System Status</h2>
141
-
146
+
142
147
<div>
143
148
<h3>Database</h3>
144
149
<p>Status: Online</p>
@@ -152,6 +157,7 @@ import { FocusTrap } from '@radix-ui/react-focus-trap';
152
157
```
153
158
154
159
### Loading States and Error Messages
160
+
155
161
```typescript
156
162
// ✅ Include accessible loading states
157
163
{isLoading && (
@@ -177,6 +183,7 @@ import { FocusTrap } from '@radix-ui/react-focus-trap';
177
183
## Testing Patterns
178
184
179
185
### Component Testing with React Testing Library
186
+
180
187
```typescript
181
188
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
182
189
import userEvent from '@testing-library/user-event';
@@ -191,27 +198,27 @@ describe('UserProfile', () => {
191
198
192
199
it('renders user information correctly', () => {
193
200
render(<UserProfile user={mockUser} />);
194
-
201
+
195
202
expect(screen.getByText('John Doe')).toBeInTheDocument();
196
203
expect(screen.getByText('
[email protected] ')).toBeInTheDocument();
197
204
});
198
205
199
206
it('allows editing user name', async () => {
200
207
const user = userEvent.setup();
201
208
const onSave = jest.fn();
202
-
209
+
203
210
render(<UserProfile user={mockUser} onSave={onSave} />);
204
-
211
+
205
212
const editButton = screen.getByRole('button', { name: /edit/i });
206
213
await user.click(editButton);
207
-
214
+
208
215
const nameInput = screen.getByLabelText(/name/i);
209
216
await user.clear(nameInput);
210
217
await user.type(nameInput, 'Jane Doe');
211
-
218
+
212
219
const saveButton = screen.getByRole('button', { name: /save/i });
213
220
await user.click(saveButton);
214
-
221
+
215
222
await waitFor(() => {
216
223
expect(onSave).toHaveBeenCalledWith({
217
224
...mockUser,
@@ -223,58 +230,60 @@ describe('UserProfile', () => {
223
230
```
224
231
225
232
### Custom Hook Testing
233
+
226
234
```typescript
227
- import { renderHook, act } from ' @testing-library/react' ;
228
- import { useEntityList } from ' ./useEntityList' ;
235
+ import { renderHook, act } from " @testing-library/react" ;
236
+ import { useEntityList } from " ./useEntityList" ;
229
237
230
- describe(' useEntityList' , () => {
231
- it(' fetches entities successfully' , async () => {
238
+ describe(" useEntityList" , () => {
239
+ it(" fetches entities successfully" , async () => {
232
240
const mockData = {
233
- content: [{ id: '1' , name: ' Entity 1' }],
241
+ content: [{ id: "1" , name: " Entity 1" }],
234
242
total: 1,
235
243
};
236
-
244
+
237
245
// Mock API call
238
246
jest.mocked(api.get).mockResolvedValue({ data: mockData });
239
-
247
+
240
248
const { result } = renderHook(() =>
241
- useEntityList({ workspaceName: ' test' , page: 1, size: 10 })
249
+ useEntityList({ workspaceName: " test" , page: 1, size: 10 }),
242
250
);
243
-
251
+
244
252
await waitFor(() => {
245
253
expect(result.current.isSuccess).toBe(true);
246
254
});
247
-
255
+
248
256
expect(result.current.data).toEqual(mockData);
249
257
});
250
258
});
251
259
```
252
260
253
261
### API Mocking with MSW
262
+
254
263
```typescript
255
264
// src/mocks/handlers.ts
256
- import { rest } from ' msw' ;
265
+ import { rest } from " msw" ;
257
266
258
267
export const handlers = [
259
- rest.get(' /api/v1/entities' , (req, res, ctx) => {
268
+ rest.get(" /api/v1/entities" , (req, res, ctx) => {
260
269
return res(
261
270
ctx.json({
262
271
content: [
263
- { id: '1' , name: ' Entity 1' },
264
- { id: '2' , name: ' Entity 2' },
272
+ { id: "1" , name: " Entity 1" },
273
+ { id: "2" , name: " Entity 2" },
265
274
],
266
275
total: 2,
267
- })
276
+ }),
268
277
);
269
278
}),
270
-
271
- rest.delete(' /api/v1/entities/:id' , (req, res, ctx) => {
279
+
280
+ rest.delete(" /api/v1/entities/:id" , (req, res, ctx) => {
272
281
return res(ctx.status(204));
273
282
}),
274
283
];
275
284
276
285
// In test files
277
- import { server } from ' ../mocks/server' ;
286
+ import { server } from " ../mocks/server" ;
278
287
279
288
beforeEach(() => {
280
289
server.listen();
@@ -290,6 +299,7 @@ afterAll(() => {
290
299
```
291
300
292
301
### Integration Testing
302
+
293
303
```typescript
294
304
import { render, screen, waitFor } from '@testing-library/react';
295
305
import userEvent from '@testing-library/user-event';
@@ -303,7 +313,7 @@ const createWrapper = () => {
303
313
mutations: { retry: false },
304
314
},
305
315
});
306
-
316
+
307
317
return ({ children }: { children: React.ReactNode }) => (
308
318
<QueryClientProvider client={queryClient}>
309
319
{children}
@@ -314,33 +324,33 @@ const createWrapper = () => {
314
324
describe('EntityListPage Integration', () => {
315
325
it('loads and displays entities', async () => {
316
326
render(<EntityListPage />, { wrapper: createWrapper() });
317
-
327
+
318
328
// Wait for loading to complete
319
329
await waitFor(() => {
320
330
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
321
331
});
322
-
332
+
323
333
// Check if entities are displayed
324
334
expect(screen.getByText('Entity 1')).toBeInTheDocument();
325
335
expect(screen.getByText('Entity 2')).toBeInTheDocument();
326
336
});
327
-
337
+
328
338
it('handles entity deletion flow', async () => {
329
339
const user = userEvent.setup();
330
340
render(<EntityListPage />, { wrapper: createWrapper() });
331
-
341
+
332
342
await waitFor(() => {
333
343
expect(screen.getByText('Entity 1')).toBeInTheDocument();
334
344
});
335
-
345
+
336
346
// Find and click delete button
337
347
const deleteButton = screen.getByRole('button', { name: /delete entity 1/i });
338
348
await user.click(deleteButton);
339
-
349
+
340
350
// Confirm deletion
341
351
const confirmButton = screen.getByRole('button', { name: /confirm/i });
342
352
await user.click(confirmButton);
343
-
353
+
344
354
// Check if entity is removed
345
355
await waitFor(() => {
346
356
expect(screen.queryByText('Entity 1')).not.toBeInTheDocument();
@@ -360,7 +370,7 @@ describe('EntityListPage Integration', () => {
360
370
361
371
```typescript
362
372
// ✅ Good: Testing behavior
363
- expect(screen.getByRole(' button' , { name: /save/i })).toBeEnabled();
373
+ expect(screen.getByRole(" button" , { name: /save/i })).toBeEnabled();
364
374
365
375
// ❌ Avoid: Testing implementation
366
376
expect(component.state.isSaving).toBe(false);
@@ -370,5 +380,5 @@ expect(screen.getByLabelText(/email address/i)).toBeInTheDocument();
370
380
371
381
// ✅ Good: Testing error states
372
382
await user.click(submitButton);
373
- expect(screen.getByRole(' alert' )).toHaveTextContent(' Email is required' );
374
- ```
383
+ expect(screen.getByRole(" alert" )).toHaveTextContent(" Email is required" );
384
+ ```
0 commit comments