@fpkit/acss 1.0.0-beta.1 → 2.0.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.
Files changed (103) hide show
  1. package/README.md +92 -0
  2. package/docs/README.md +325 -0
  3. package/docs/guides/accessibility.md +764 -0
  4. package/docs/guides/architecture.md +705 -0
  5. package/docs/guides/composition.md +688 -0
  6. package/docs/guides/css-variables.md +522 -0
  7. package/docs/guides/storybook.md +828 -0
  8. package/docs/guides/testing.md +817 -0
  9. package/docs/testing/focus-indicator-testing.md +437 -0
  10. package/libs/{chunk-7XPFW7CB.js → chunk-43TK2ICH.js} +2 -2
  11. package/libs/chunk-5PJYLVFY.cjs +17 -0
  12. package/libs/chunk-5PJYLVFY.cjs.map +1 -0
  13. package/libs/chunk-E4OSROCA.cjs +17 -0
  14. package/libs/chunk-E4OSROCA.cjs.map +1 -0
  15. package/libs/chunk-KVKQLRJG.js +10 -0
  16. package/libs/chunk-KVKQLRJG.js.map +1 -0
  17. package/libs/{chunk-QVW6W76L.cjs → chunk-MGPWZRBX.cjs} +3 -3
  18. package/libs/chunk-NNTBIHSD.js +8 -0
  19. package/libs/chunk-NNTBIHSD.js.map +1 -0
  20. package/libs/{chunk-X3JCTEPD.js → chunk-QKHPHMG2.js} +2 -2
  21. package/libs/{chunk-T4T6GWYQ.cjs → chunk-R7NLLZU2.cjs} +3 -3
  22. package/libs/{chunk-X5LGFCWG.js → chunk-UJAQVHWC.js} +3 -3
  23. package/libs/{chunk-DKTHCQ5P.cjs → chunk-X5RKCLDC.cjs} +3 -3
  24. package/libs/components/breadcrumbs/breadcrumb.cjs +5 -5
  25. package/libs/components/breadcrumbs/breadcrumb.d.cts +1 -1
  26. package/libs/components/breadcrumbs/breadcrumb.d.ts +1 -1
  27. package/libs/components/breadcrumbs/breadcrumb.js +2 -2
  28. package/libs/components/button.cjs +3 -3
  29. package/libs/components/button.d.cts +1 -1
  30. package/libs/components/button.d.ts +1 -1
  31. package/libs/components/button.js +1 -1
  32. package/libs/components/buttons/button.css +1 -1
  33. package/libs/components/buttons/button.css.map +1 -1
  34. package/libs/components/buttons/button.min.css +2 -2
  35. package/libs/components/dialog/dialog.cjs +4 -4
  36. package/libs/components/dialog/dialog.js +2 -2
  37. package/libs/components/icons/icon.d.cts +32 -32
  38. package/libs/components/icons/icon.d.ts +32 -32
  39. package/libs/components/link/link.cjs +11 -3
  40. package/libs/components/link/link.d.cts +131 -3
  41. package/libs/components/link/link.d.ts +131 -3
  42. package/libs/components/link/link.js +1 -1
  43. package/libs/components/list/list.css +1 -1
  44. package/libs/components/list/list.min.css +1 -1
  45. package/libs/components/modal.cjs +3 -3
  46. package/libs/components/modal.js +2 -2
  47. package/libs/hooks.cjs +3 -3
  48. package/libs/hooks.d.cts +1 -1
  49. package/libs/hooks.d.ts +1 -1
  50. package/libs/hooks.js +2 -2
  51. package/libs/index.cjs +12 -12
  52. package/libs/index.css +1 -1
  53. package/libs/index.css.map +1 -1
  54. package/libs/index.d.cts +237 -2
  55. package/libs/index.d.ts +237 -2
  56. package/libs/index.js +5 -5
  57. package/package.json +4 -3
  58. package/src/components/README.mdx +1 -1
  59. package/src/components/breadcrumbs/breadcrumb.test.tsx +1 -2
  60. package/src/components/buttons/README.mdx +19 -9
  61. package/src/components/buttons/button.scss +5 -0
  62. package/src/components/buttons/button.stories.tsx +8 -5
  63. package/src/components/buttons/button.tsx +19 -15
  64. package/src/components/cards/card.stories.tsx +1 -1
  65. package/src/components/details/details.stories.tsx +1 -1
  66. package/src/components/form/form.stories.tsx +1 -1
  67. package/src/components/form/input.stories.tsx +1 -1
  68. package/src/components/form/select.stories.tsx +1 -1
  69. package/src/components/heading/README.mdx +292 -0
  70. package/src/components/icons/icon.stories.tsx +1 -1
  71. package/src/components/link/link.stories.tsx +205 -8
  72. package/src/components/link/link.test.tsx +1 -1
  73. package/src/components/link/link.tsx +22 -0
  74. package/src/components/link/link.types.ts +11 -3
  75. package/src/components/list/list.scss +1 -1
  76. package/src/components/nav/nav.stories.tsx +1 -1
  77. package/src/components/ui.stories.tsx +53 -19
  78. package/src/docs/accessibility.mdx +484 -0
  79. package/src/docs/composition.mdx +549 -0
  80. package/src/docs/css-variables.mdx +380 -0
  81. package/src/docs/fpkit-developer.mdx +623 -0
  82. package/src/introduction.mdx +356 -0
  83. package/src/styles/buttons/button.css +4 -0
  84. package/src/styles/buttons/button.css.map +1 -1
  85. package/src/styles/index.css +9 -3
  86. package/src/styles/index.css.map +1 -1
  87. package/src/styles/list/list.css +1 -1
  88. package/src/styles/utilities/_disabled.scss +5 -4
  89. package/libs/chunk-33PNJ4LO.cjs +0 -15
  90. package/libs/chunk-33PNJ4LO.cjs.map +0 -1
  91. package/libs/chunk-GT77BX4L.cjs +0 -17
  92. package/libs/chunk-GT77BX4L.cjs.map +0 -1
  93. package/libs/chunk-OVWLQYMK.js +0 -10
  94. package/libs/chunk-OVWLQYMK.js.map +0 -1
  95. package/libs/chunk-UEPAWMDF.js +0 -8
  96. package/libs/chunk-UEPAWMDF.js.map +0 -1
  97. package/libs/link-5192f411.d.ts +0 -323
  98. /package/libs/{chunk-7XPFW7CB.js.map → chunk-43TK2ICH.js.map} +0 -0
  99. /package/libs/{chunk-QVW6W76L.cjs.map → chunk-MGPWZRBX.cjs.map} +0 -0
  100. /package/libs/{chunk-X3JCTEPD.js.map → chunk-QKHPHMG2.js.map} +0 -0
  101. /package/libs/{chunk-T4T6GWYQ.cjs.map → chunk-R7NLLZU2.cjs.map} +0 -0
  102. /package/libs/{chunk-X5LGFCWG.js.map → chunk-UJAQVHWC.js.map} +0 -0
  103. /package/libs/{chunk-DKTHCQ5P.cjs.map → chunk-X5RKCLDC.cjs.map} +0 -0
@@ -0,0 +1,817 @@
1
+ # Testing Guide
2
+
3
+ ## Overview
4
+
5
+ This guide shows how to test applications and custom components built with @fpkit/acss using **Vitest** and **React Testing Library**. fpkit components are already tested, so focus your tests on your custom logic, compositions, and integrations.
6
+
7
+ ---
8
+
9
+ ## Setup
10
+
11
+ ### Installation
12
+
13
+ ```bash
14
+ npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
15
+ ```
16
+
17
+ ### Vitest Configuration
18
+
19
+ ```javascript
20
+ // vitest.config.ts
21
+ import { defineConfig } from 'vitest/config'
22
+ import react from '@vitejs/plugin-react'
23
+
24
+ export default defineConfig({
25
+ plugins: [react()],
26
+ test: {
27
+ environment: 'jsdom',
28
+ globals: true,
29
+ setupFiles: './src/test/setup.ts',
30
+ },
31
+ })
32
+ ```
33
+
34
+ ### Setup File
35
+
36
+ ```typescript
37
+ // src/test/setup.ts
38
+ import { expect, afterEach } from 'vitest'
39
+ import { cleanup } from '@testing-library/react'
40
+ import * as matchers from '@testing-library/jest-dom/matchers'
41
+
42
+ expect.extend(matchers)
43
+
44
+ afterEach(() => {
45
+ cleanup()
46
+ })
47
+ ```
48
+
49
+ ### Package.json Scripts
50
+
51
+ ```json
52
+ {
53
+ "scripts": {
54
+ "test": "vitest",
55
+ "test:ui": "vitest --ui",
56
+ "test:coverage": "vitest --coverage"
57
+ }
58
+ }
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Basic Testing Patterns
64
+
65
+ ### Rendering Tests
66
+
67
+ Test that your composed components render correctly:
68
+
69
+ ```typescript
70
+ import { render, screen } from '@testing-library/react'
71
+ import { describe, it, expect } from 'vitest'
72
+ import { Button } from '@fpkit/acss'
73
+
74
+ describe('Custom Button Usage', () => {
75
+ it('renders button with custom content', () => {
76
+ render(<Button>Click me</Button>)
77
+ expect(screen.getByText('Click me')).toBeInTheDocument()
78
+ })
79
+
80
+ it('renders button with custom className', () => {
81
+ render(<Button className="custom-btn">Click me</Button>)
82
+ const button = screen.getByRole('button')
83
+ expect(button).toHaveClass('custom-btn')
84
+ })
85
+ })
86
+ ```
87
+
88
+ ### Testing Composed Components
89
+
90
+ Focus tests on how your components integrate with fpkit:
91
+
92
+ ```typescript
93
+ import { Badge, Button } from '@fpkit/acss'
94
+
95
+ // Your composed component
96
+ const StatusButton = ({ status, children }) => (
97
+ <Button>
98
+ {children}
99
+ <Badge variant={status}>{status}</Badge>
100
+ </Button>
101
+ )
102
+
103
+ // Your tests
104
+ describe('StatusButton', () => {
105
+ it('renders button with status badge', () => {
106
+ render(<StatusButton status="active">Server</StatusButton>)
107
+
108
+ // Test composition
109
+ expect(screen.getByRole('button')).toBeInTheDocument()
110
+ expect(screen.getByText('Server')).toBeInTheDocument()
111
+ expect(screen.getByText('active')).toBeInTheDocument()
112
+ })
113
+
114
+ it('passes props to underlying button', () => {
115
+ const handleClick = vi.fn()
116
+ render(
117
+ <StatusButton status="inactive" onClick={handleClick}>
118
+ Server
119
+ </StatusButton>
120
+ )
121
+
122
+ const button = screen.getByRole('button')
123
+ await userEvent.click(button)
124
+
125
+ expect(handleClick).toHaveBeenCalledTimes(1)
126
+ })
127
+ })
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Query Best Practices
133
+
134
+ ### Query Priority
135
+
136
+ Use queries in this order:
137
+
138
+ 1. **getByRole** (best) - Most accessible query
139
+ 2. **getByLabelText** - For form controls
140
+ 3. **getByText** - For non-interactive content
141
+ 4. **getByTestId** (last resort) - When no other query works
142
+
143
+ ```typescript
144
+ // ✅ Good - query by role
145
+ const button = screen.getByRole('button', { name: 'Submit' })
146
+
147
+ // ✅ Good - query by label
148
+ const input = screen.getByLabelText('Email')
149
+
150
+ // ✅ Good - query by text
151
+ const heading = screen.getByText('Welcome')
152
+
153
+ // ⚠️ Use sparingly - test ID
154
+ const custom = screen.getByTestId('custom-element')
155
+ ```
156
+
157
+ ### Common Queries
158
+
159
+ ```typescript
160
+ // Buttons
161
+ screen.getByRole('button')
162
+ screen.getByRole('button', { name: 'Submit' })
163
+
164
+ // Links
165
+ screen.getByRole('link', { name: 'Home' })
166
+
167
+ // Headings
168
+ screen.getByRole('heading', { level: 1 })
169
+ screen.getByRole('heading', { name: 'Title' })
170
+
171
+ // Form controls
172
+ screen.getByLabelText('Email')
173
+ screen.getByPlaceholderText('Enter email...')
174
+ screen.getByRole('textbox')
175
+
176
+ // Text content
177
+ screen.getByText('Hello')
178
+ screen.getByText(/hello/i) // Case-insensitive
179
+
180
+ // Test IDs
181
+ screen.getByTestId('custom-id')
182
+ ```
183
+
184
+ ### Multiple Elements
185
+
186
+ ```typescript
187
+ // Get all matching elements
188
+ const buttons = screen.getAllByRole('button')
189
+ expect(buttons).toHaveLength(3)
190
+
191
+ // Query (returns null if not found)
192
+ const button = screen.queryByRole('button')
193
+ expect(button).not.toBeInTheDocument()
194
+
195
+ // Find (async, waits for element)
196
+ const button = await screen.findByRole('button')
197
+ expect(button).toBeInTheDocument()
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Event Testing
203
+
204
+ ### User Interactions
205
+
206
+ ```typescript
207
+ import { userEvent } from '@testing-library/user-event'
208
+
209
+ describe('User Interactions', () => {
210
+ it('calls onClick handler when clicked', async () => {
211
+ const handleClick = vi.fn()
212
+ render(<Button onClick={handleClick}>Click me</Button>)
213
+
214
+ const button = screen.getByRole('button')
215
+ await userEvent.click(button)
216
+
217
+ expect(handleClick).toHaveBeenCalledTimes(1)
218
+ })
219
+
220
+ it('handles keyboard activation', async () => {
221
+ const handleClick = vi.fn()
222
+ render(<Button onClick={handleClick}>Click me</Button>)
223
+
224
+ const button = screen.getByRole('button')
225
+ button.focus()
226
+ await userEvent.keyboard('{Enter}')
227
+
228
+ expect(handleClick).toHaveBeenCalled()
229
+ })
230
+
231
+ it('handles hover events', async () => {
232
+ const handleHover = vi.fn()
233
+ render(<Button onMouseEnter={handleHover}>Hover me</Button>)
234
+
235
+ const button = screen.getByRole('button')
236
+ await userEvent.hover(button)
237
+
238
+ expect(handleHover).toHaveBeenCalled()
239
+ })
240
+ })
241
+ ```
242
+
243
+ ### Form Interactions
244
+
245
+ ```typescript
246
+ describe('Form Interactions', () => {
247
+ it('handles text input', async () => {
248
+ const handleChange = vi.fn()
249
+ render(<Input onChange={handleChange} />)
250
+
251
+ const input = screen.getByRole('textbox')
252
+ await userEvent.type(input, 'Hello')
253
+
254
+ expect(input).toHaveValue('Hello')
255
+ expect(handleChange).toHaveBeenCalledTimes(5) // Once per character
256
+ })
257
+
258
+ it('handles form submission', async () => {
259
+ const handleSubmit = vi.fn((e) => e.preventDefault())
260
+ render(
261
+ <form onSubmit={handleSubmit}>
262
+ <Input name="email" />
263
+ <Button type="submit">Submit</Button>
264
+ </form>
265
+ )
266
+
267
+ const input = screen.getByRole('textbox')
268
+ const submitBtn = screen.getByRole('button')
269
+
270
+ await userEvent.type(input, 'test@example.com')
271
+ await userEvent.click(submitBtn)
272
+
273
+ expect(handleSubmit).toHaveBeenCalled()
274
+ })
275
+ })
276
+ ```
277
+
278
+ ---
279
+
280
+ ## Testing Component States
281
+
282
+ ### Disabled State
283
+
284
+ ```typescript
285
+ describe('Disabled State', () => {
286
+ it('has aria-disabled attribute when disabled', () => {
287
+ render(<Button disabled>Click me</Button>)
288
+ const button = screen.getByRole('button')
289
+ expect(button).toHaveAttribute('aria-disabled', 'true')
290
+ })
291
+
292
+ it('does not call onClick when disabled', async () => {
293
+ const handleClick = vi.fn()
294
+ render(<Button disabled onClick={handleClick}>Click me</Button>)
295
+
296
+ const button = screen.getByRole('button')
297
+ await userEvent.click(button)
298
+
299
+ // fpkit buttons with aria-disabled will call onClick
300
+ // but you can prevent it in your wrapper
301
+ // Test your specific implementation
302
+ })
303
+ })
304
+ ```
305
+
306
+ ### Loading State
307
+
308
+ ```typescript
309
+ const LoadingButton = ({ loading, onClick, children }) => {
310
+ return (
311
+ <Button disabled={loading} onClick={onClick}>
312
+ {loading ? 'Loading...' : children}
313
+ </Button>
314
+ )
315
+ }
316
+
317
+ describe('LoadingButton', () => {
318
+ it('shows loading text when loading', () => {
319
+ render(<LoadingButton loading>Submit</LoadingButton>)
320
+ expect(screen.getByText('Loading...')).toBeInTheDocument()
321
+ expect(screen.queryByText('Submit')).not.toBeInTheDocument()
322
+ })
323
+
324
+ it('disables button while loading', () => {
325
+ render(<LoadingButton loading>Submit</LoadingButton>)
326
+ const button = screen.getByRole('button')
327
+ expect(button).toHaveAttribute('aria-disabled', 'true')
328
+ })
329
+ })
330
+ ```
331
+
332
+ ### Conditional Rendering
333
+
334
+ ```typescript
335
+ describe('Conditional Rendering', () => {
336
+ it('shows error message when error exists', () => {
337
+ const { rerender } = render(<Input />)
338
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument()
339
+
340
+ rerender(<Input error="Invalid input" />)
341
+ expect(screen.getByRole('alert')).toHaveTextContent('Invalid input')
342
+ })
343
+
344
+ it('renders different content based on prop', () => {
345
+ const { rerender } = render(
346
+ <Notification inline>Message</Notification>
347
+ )
348
+ expect(screen.getByRole('alert')).toBeInTheDocument()
349
+
350
+ rerender(<Notification isOpen>Message</Notification>)
351
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
352
+ })
353
+ })
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Accessibility Testing
359
+
360
+ ### ARIA Attributes
361
+
362
+ ```typescript
363
+ describe('Accessibility', () => {
364
+ it('has correct ARIA label', () => {
365
+ render(
366
+ <Button aria-label="Close dialog">
367
+ <Icon name="close" />
368
+ </Button>
369
+ )
370
+
371
+ const button = screen.getByRole('button', { name: 'Close dialog' })
372
+ expect(button).toBeInTheDocument()
373
+ })
374
+
375
+ it('has correct ARIA description', () => {
376
+ render(
377
+ <>
378
+ <Button aria-describedby="hint">Submit</Button>
379
+ <div id="hint">This will save your changes</div>
380
+ </>
381
+ )
382
+
383
+ const button = screen.getByRole('button')
384
+ expect(button).toHaveAttribute('aria-describedby', 'hint')
385
+ })
386
+ })
387
+ ```
388
+
389
+ ### Keyboard Navigation
390
+
391
+ ```typescript
392
+ describe('Keyboard Navigation', () => {
393
+ it('is focusable with Tab key', async () => {
394
+ render(<Button>Click me</Button>)
395
+
396
+ const button = screen.getByRole('button')
397
+ expect(button).not.toHaveFocus()
398
+
399
+ await userEvent.tab()
400
+ expect(button).toHaveFocus()
401
+ })
402
+
403
+ it('navigates through multiple buttons', async () => {
404
+ render(
405
+ <>
406
+ <Button>First</Button>
407
+ <Button>Second</Button>
408
+ <Button>Third</Button>
409
+ </>
410
+ )
411
+
412
+ const [first, second, third] = screen.getAllByRole('button')
413
+
414
+ await userEvent.tab()
415
+ expect(first).toHaveFocus()
416
+
417
+ await userEvent.tab()
418
+ expect(second).toHaveFocus()
419
+
420
+ await userEvent.tab()
421
+ expect(third).toHaveFocus()
422
+ })
423
+
424
+ it('closes dialog on Escape key', async () => {
425
+ const handleClose = vi.fn()
426
+ render(
427
+ <Dialog isOpen onClose={handleClose}>
428
+ Content
429
+ </Dialog>
430
+ )
431
+
432
+ await userEvent.keyboard('{Escape}')
433
+ expect(handleClose).toHaveBeenCalled()
434
+ })
435
+ })
436
+ ```
437
+
438
+ ### Automated Accessibility Testing
439
+
440
+ ```bash
441
+ npm install -D jest-axe
442
+ ```
443
+
444
+ ```typescript
445
+ import { axe, toHaveNoViolations } from 'jest-axe'
446
+
447
+ expect.extend(toHaveNoViolations)
448
+
449
+ describe('Accessibility Violations', () => {
450
+ it('should not have accessibility violations', async () => {
451
+ const { container } = render(
452
+ <div>
453
+ <Button>Click me</Button>
454
+ <Link href="/page">Navigate</Link>
455
+ </div>
456
+ )
457
+
458
+ const results = await axe(container)
459
+ expect(results).toHaveNoViolations()
460
+ })
461
+
462
+ it('composed component has no violations', async () => {
463
+ const { container } = render(
464
+ <StatusButton status="active">Server</StatusButton>
465
+ )
466
+
467
+ const results = await axe(container)
468
+ expect(results).toHaveNoViolations()
469
+ })
470
+ })
471
+ ```
472
+
473
+ ---
474
+
475
+ ## Async Testing
476
+
477
+ ### Waiting for Elements
478
+
479
+ ```typescript
480
+ describe('Async Rendering', () => {
481
+ it('shows success message after action', async () => {
482
+ const SuccessComponent = () => {
483
+ const [success, setSuccess] = useState(false)
484
+ return (
485
+ <>
486
+ <Button onClick={() => setTimeout(() => setSuccess(true), 100)}>
487
+ Submit
488
+ </Button>
489
+ {success && <div role="alert">Success!</div>}
490
+ </>
491
+ )
492
+ }
493
+
494
+ render(<SuccessComponent />)
495
+
496
+ const button = screen.getByRole('button')
497
+ await userEvent.click(button)
498
+
499
+ // Wait for success message to appear
500
+ const alert = await screen.findByRole('alert')
501
+ expect(alert).toHaveTextContent('Success!')
502
+ })
503
+ })
504
+ ```
505
+
506
+ ### Testing Loading States
507
+
508
+ ```typescript
509
+ import { waitFor } from '@testing-library/react'
510
+
511
+ describe('Loading States', () => {
512
+ it('shows loading then content', async () => {
513
+ const DataComponent = () => {
514
+ const [loading, setLoading] = useState(true)
515
+ const [data, setData] = useState(null)
516
+
517
+ useEffect(() => {
518
+ setTimeout(() => {
519
+ setData('Loaded data')
520
+ setLoading(false)
521
+ }, 100)
522
+ }, [])
523
+
524
+ if (loading) return <div>Loading...</div>
525
+ return <div>{data}</div>
526
+ }
527
+
528
+ render(<DataComponent />)
529
+
530
+ // Initially shows loading
531
+ expect(screen.getByText('Loading...')).toBeInTheDocument()
532
+
533
+ // Wait for loading to disappear
534
+ await waitFor(() => {
535
+ expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
536
+ })
537
+
538
+ // Data is displayed
539
+ expect(screen.getByText('Loaded data')).toBeInTheDocument()
540
+ })
541
+ })
542
+ ```
543
+
544
+ ---
545
+
546
+ ## Testing Compound Components
547
+
548
+ ```typescript
549
+ import { Card } from '@fpkit/acss'
550
+
551
+ describe('Card Component Usage', () => {
552
+ it('renders card with all sub-components', () => {
553
+ render(
554
+ <Card>
555
+ <Card.Header>
556
+ <Card.Title>Title</Card.Title>
557
+ </Card.Header>
558
+ <Card.Content>Content</Card.Content>
559
+ <Card.Footer>
560
+ <Button>Action</Button>
561
+ </Card.Footer>
562
+ </Card>
563
+ )
564
+
565
+ expect(screen.getByText('Title')).toBeInTheDocument()
566
+ expect(screen.getByText('Content')).toBeInTheDocument()
567
+ expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument()
568
+ })
569
+
570
+ it('renders card without optional sections', () => {
571
+ render(
572
+ <Card>
573
+ <Card.Content>Just content</Card.Content>
574
+ </Card>
575
+ )
576
+
577
+ expect(screen.getByText('Just content')).toBeInTheDocument()
578
+ // Header and footer should not exist
579
+ })
580
+ })
581
+ ```
582
+
583
+ ---
584
+
585
+ ## Mock Functions
586
+
587
+ ### Creating and Using Mocks
588
+
589
+ ```typescript
590
+ import { vi } from 'vitest'
591
+
592
+ describe('Mocking', () => {
593
+ it('mocks onClick handler', async () => {
594
+ const handleClick = vi.fn()
595
+ render(<Button onClick={handleClick}>Click me</Button>)
596
+
597
+ await userEvent.click(screen.getByRole('button'))
598
+
599
+ expect(handleClick).toHaveBeenCalledTimes(1)
600
+ })
601
+
602
+ it('mocks with return value', () => {
603
+ const mockFn = vi.fn(() => 'mocked value')
604
+ const result = mockFn()
605
+
606
+ expect(result).toBe('mocked value')
607
+ expect(mockFn).toHaveBeenCalled()
608
+ })
609
+
610
+ it('mocks with arguments', async () => {
611
+ const handleChange = vi.fn()
612
+ render(<Input onChange={handleChange} />)
613
+
614
+ const input = screen.getByRole('textbox')
615
+ await userEvent.type(input, 'test')
616
+
617
+ expect(handleChange).toHaveBeenCalledWith(
618
+ expect.objectContaining({
619
+ target: expect.objectContaining({ value: 't' })
620
+ })
621
+ )
622
+ })
623
+ })
624
+ ```
625
+
626
+ ---
627
+
628
+ ## Test Organization
629
+
630
+ ### Describe Blocks
631
+
632
+ ```typescript
633
+ describe('CustomComponent', () => {
634
+ describe('rendering', () => {
635
+ it('renders with default props', () => {
636
+ // Test
637
+ })
638
+
639
+ it('renders with custom props', () => {
640
+ // Test
641
+ })
642
+ })
643
+
644
+ describe('interactions', () => {
645
+ it('handles click events', async () => {
646
+ // Test
647
+ })
648
+
649
+ it('handles keyboard events', async () => {
650
+ // Test
651
+ })
652
+ })
653
+
654
+ describe('accessibility', () => {
655
+ it('has proper ARIA attributes', () => {
656
+ // Test
657
+ })
658
+
659
+ it('is keyboard navigable', async () => {
660
+ // Test
661
+ })
662
+ })
663
+
664
+ describe('states', () => {
665
+ it('shows loading state', () => {
666
+ // Test
667
+ })
668
+
669
+ it('shows error state', () => {
670
+ // Test
671
+ })
672
+ })
673
+ })
674
+ ```
675
+
676
+ ### Test Naming
677
+
678
+ ```typescript
679
+ // ✅ Good - descriptive and clear
680
+ it('calls onClick handler when button is clicked')
681
+ it('shows error message when validation fails')
682
+ it('disables submit button while form is submitting')
683
+ it('renders badge with correct variant')
684
+
685
+ // ❌ Bad - vague or redundant
686
+ it('works correctly')
687
+ it('test button')
688
+ it('should show message') // "should" is redundant
689
+ ```
690
+
691
+ ---
692
+
693
+ ## Common Testing Patterns
694
+
695
+ ### Testing Custom Props
696
+
697
+ ```typescript
698
+ const CustomButton = ({ loading, error, ...props }) => (
699
+ <Button
700
+ {...props}
701
+ disabled={loading}
702
+ style={{ '--btn-bg': error ? 'red' : undefined }}
703
+ >
704
+ {loading ? 'Loading...' : props.children}
705
+ </Button>
706
+ )
707
+
708
+ describe('CustomButton Props', () => {
709
+ it('shows loading state', () => {
710
+ render(<CustomButton loading>Submit</CustomButton>)
711
+ expect(screen.getByText('Loading...')).toBeInTheDocument()
712
+ })
713
+
714
+ it('applies error styling', () => {
715
+ render(<CustomButton error>Submit</CustomButton>)
716
+ const button = screen.getByRole('button')
717
+ expect(button).toHaveStyle({ '--btn-bg': 'red' })
718
+ })
719
+ })
720
+ ```
721
+
722
+ ### Testing Composition
723
+
724
+ ```typescript
725
+ const ActionCard = ({ title, onAction }) => (
726
+ <Card>
727
+ <Card.Title>{title}</Card.Title>
728
+ <Card.Footer>
729
+ <Button onClick={onAction}>Perform Action</Button>
730
+ </Card.Footer>
731
+ </Card>
732
+ )
733
+
734
+ describe('ActionCard Composition', () => {
735
+ it('renders composed structure', () => {
736
+ render(<ActionCard title="Test Card" onAction={vi.fn()} />)
737
+
738
+ expect(screen.getByText('Test Card')).toBeInTheDocument()
739
+ expect(screen.getByRole('button')).toBeInTheDocument()
740
+ })
741
+
742
+ it('calls action handler', async () => {
743
+ const handleAction = vi.fn()
744
+ render(<ActionCard title="Test" onAction={handleAction} />)
745
+
746
+ await userEvent.click(screen.getByRole('button'))
747
+ expect(handleAction).toHaveBeenCalled()
748
+ })
749
+ })
750
+ ```
751
+
752
+ ---
753
+
754
+ ## Best Practices
755
+
756
+ ### ✅ Do
757
+
758
+ - **Test behavior, not implementation** - Focus on what users experience
759
+ - **Use accessible queries** - `getByRole`, `getByLabelText`
760
+ - **Test integration** - How components work together
761
+ - **Test user interactions** - Clicks, typing, keyboard navigation
762
+ - **Test accessibility** - ARIA attributes, keyboard support
763
+ - **Use meaningful test names** - Describe what's being tested
764
+ - **Keep tests focused** - One concept per test
765
+
766
+ ### ❌ Don't
767
+
768
+ - **Don't test fpkit internals** - fpkit components are already tested
769
+ - **Don't test styling details** - Unless critical to functionality
770
+ - **Don't use implementation details** - Avoid querying by class names
771
+ - **Don't test third-party libraries** - Trust they're tested
772
+ - **Don't write redundant tests** - If fpkit tests it, you don't need to
773
+
774
+ ---
775
+
776
+ ## Running Tests
777
+
778
+ ```bash
779
+ # Run all tests
780
+ npm test
781
+
782
+ # Run tests in watch mode
783
+ npm test -- --watch
784
+
785
+ # Run with coverage
786
+ npm run test:coverage
787
+
788
+ # Run with UI
789
+ npm run test:ui
790
+
791
+ # Run specific test file
792
+ npm test -- button.test.tsx
793
+
794
+ # Update snapshots
795
+ npm test -- -u
796
+ ```
797
+
798
+ ---
799
+
800
+ ## Additional Resources
801
+
802
+ - **[Vitest Documentation](https://vitest.dev/)** - Test runner
803
+ - **[Testing Library Docs](https://testing-library.com/docs/react-testing-library/intro/)** - Query and interaction APIs
804
+ - **[jest-axe](https://github.com/nickcolley/jest-axe)** - Automated accessibility testing
805
+ - **[Common Testing Mistakes](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)** - Best practices
806
+
807
+ ---
808
+
809
+ ## Related Guides
810
+
811
+ - **[Accessibility Guide](./accessibility.md)** - Accessibility patterns to test
812
+ - **[Composition Guide](./composition.md)** - Patterns to test in compositions
813
+ - **[Architecture Guide](./architecture.md)** - Component patterns and structure
814
+
815
+ ---
816
+
817
+ **Remember**: Focus your tests on **your application logic and composed components**. fpkit components are thoroughly tested, so trust their functionality and test how you use them together.