@moontra/moonui 2.0.7 → 2.0.9

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 (94) hide show
  1. package/package.json +26 -19
  2. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  3. package/src/__tests__/use-local-storage.test.tsx +174 -0
  4. package/src/__tests__/use-pro-access.test.tsx +183 -0
  5. package/src/components/ui/__tests__/accordion.test.tsx +571 -0
  6. package/src/components/ui/__tests__/alert.test.tsx +484 -0
  7. package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
  8. package/src/components/ui/__tests__/avatar.test.tsx +382 -0
  9. package/src/components/ui/__tests__/badge.test.tsx +364 -0
  10. package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
  11. package/src/components/ui/__tests__/button.test.tsx +91 -0
  12. package/src/components/ui/__tests__/card.test.tsx +104 -0
  13. package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
  14. package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
  15. package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
  16. package/src/components/ui/__tests__/command.test.tsx +677 -0
  17. package/src/components/ui/__tests__/data-table.test.tsx +256 -0
  18. package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
  19. package/src/components/ui/__tests__/dialog.test.tsx +764 -0
  20. package/src/components/ui/__tests__/input.test.tsx +318 -0
  21. package/src/components/ui/__tests__/label.test.tsx +103 -0
  22. package/src/components/ui/__tests__/popover.test.tsx +637 -0
  23. package/src/components/ui/__tests__/progress.test.tsx +382 -0
  24. package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
  25. package/src/components/ui/__tests__/select.test.tsx +705 -0
  26. package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
  27. package/src/components/ui/__tests__/slider.test.tsx +493 -0
  28. package/src/components/ui/__tests__/switch.test.tsx +372 -0
  29. package/src/components/ui/__tests__/table.test.tsx +824 -0
  30. package/src/components/ui/__tests__/tabs.test.tsx +887 -0
  31. package/src/components/ui/__tests__/toast.test.tsx +458 -0
  32. package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
  33. package/src/components/ui/accordion.tsx +63 -0
  34. package/src/components/ui/alert.tsx +130 -0
  35. package/src/components/ui/aspect-ratio.tsx +72 -0
  36. package/src/components/ui/avatar.tsx +135 -0
  37. package/src/components/ui/badge.tsx +225 -0
  38. package/src/components/ui/breadcrumb.tsx +207 -0
  39. package/src/components/ui/button.stories.tsx +162 -0
  40. package/src/components/ui/button.tsx +221 -0
  41. package/src/components/ui/calendar.tsx +262 -0
  42. package/src/components/ui/card.stories.tsx +208 -0
  43. package/src/components/ui/card.tsx +141 -0
  44. package/src/components/ui/checkbox.tsx +256 -0
  45. package/src/components/ui/collapsible.tsx +129 -0
  46. package/src/components/ui/color-picker.tsx +664 -0
  47. package/src/components/ui/command.tsx +218 -0
  48. package/src/components/ui/data-table.stories.tsx +509 -0
  49. package/src/components/ui/data-table.tsx +623 -0
  50. package/src/components/ui/date-picker.stories.tsx +321 -0
  51. package/src/components/ui/date-picker.tsx +603 -0
  52. package/src/components/ui/dialog.tsx +332 -0
  53. package/src/components/ui/dropdown-menu.tsx +200 -0
  54. package/src/components/ui/file-upload.tsx +400 -0
  55. package/src/components/ui/github-stars.tsx +201 -0
  56. package/src/components/ui/index.ts +12 -0
  57. package/src/components/ui/input.stories.tsx +255 -0
  58. package/src/components/ui/input.tsx +219 -0
  59. package/src/components/ui/label.tsx +26 -0
  60. package/src/components/ui/locked-component.tsx +215 -0
  61. package/src/components/ui/moon-logo.tsx +97 -0
  62. package/src/components/ui/pagination.tsx +116 -0
  63. package/src/components/ui/popover.tsx +183 -0
  64. package/src/components/ui/progress.tsx +182 -0
  65. package/src/components/ui/radio-group.tsx +243 -0
  66. package/src/components/ui/select.tsx +273 -0
  67. package/src/components/ui/separator.tsx +140 -0
  68. package/src/components/ui/simple-editor.tsx +652 -0
  69. package/src/components/ui/skeleton.tsx +351 -0
  70. package/src/components/ui/slider.tsx +351 -0
  71. package/src/components/ui/switch.tsx +83 -0
  72. package/src/components/ui/table.tsx +322 -0
  73. package/src/components/ui/tabs.tsx +195 -0
  74. package/src/components/ui/textarea.tsx +46 -0
  75. package/src/components/ui/toast.tsx +313 -0
  76. package/src/components/ui/toggle.tsx +47 -0
  77. package/src/components/ui/tooltip.tsx +152 -0
  78. package/src/docs/theme-system.md +1194 -0
  79. package/src/index.tsx +39 -0
  80. package/src/lib/micro-interactions.ts +255 -0
  81. package/src/lib/theme.tsx +398 -0
  82. package/src/lib/utils.ts +69 -0
  83. package/src/styles/design-system.css +365 -0
  84. package/src/styles/index.css +4 -0
  85. package/src/styles/tailwind.css +6 -0
  86. package/src/styles/tokens.css +453 -0
  87. package/src/use-intersection-observer.tsx +154 -0
  88. package/src/use-local-storage.tsx +71 -0
  89. package/src/use-paddle.ts +138 -0
  90. package/src/use-performance-optimizer.ts +379 -0
  91. package/src/use-pro-access.ts +141 -0
  92. package/src/use-scroll-animation.ts +221 -0
  93. package/src/use-subscription.ts +37 -0
  94. package/src/use-toast.ts +32 -0
@@ -0,0 +1,887 @@
1
+ import React from 'react'
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
3
+ import userEvent from '@testing-library/user-event'
4
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from '../tabs'
5
+
6
+ // Mock icons for testing
7
+ const MockIcon = () => <span data-testid="mock-icon">Icon</span>
8
+ const MockBadge = () => <span data-testid="mock-badge">Badge</span>
9
+
10
+ describe('Tabs Components', () => {
11
+ describe('Tabs Component', () => {
12
+ describe('Basic Rendering', () => {
13
+ it('renders correctly with default props', () => {
14
+ render(
15
+ <Tabs data-testid="tabs">
16
+ <TabsList>
17
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
18
+ </TabsList>
19
+ <TabsContent value="tab1">Content 1</TabsContent>
20
+ </Tabs>
21
+ )
22
+
23
+ const tabs = screen.getByTestId('tabs')
24
+ expect(tabs).toBeInTheDocument()
25
+ expect(tabs).toHaveAttribute('data-orientation', 'horizontal')
26
+ })
27
+
28
+ it('applies custom className', () => {
29
+ render(
30
+ <Tabs className="custom-tabs" data-testid="tabs">
31
+ <TabsList>
32
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
33
+ </TabsList>
34
+ </Tabs>
35
+ )
36
+
37
+ const tabs = screen.getByTestId('tabs')
38
+ expect(tabs).toHaveClass('custom-tabs')
39
+ })
40
+
41
+ it('forwards ref correctly', () => {
42
+ const ref = React.createRef<HTMLDivElement>()
43
+ render(
44
+ <Tabs ref={ref} data-testid="tabs">
45
+ <TabsList>
46
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
47
+ </TabsList>
48
+ </Tabs>
49
+ )
50
+
51
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
52
+ })
53
+
54
+ it('maintains displayName', () => {
55
+ expect(Tabs.displayName).toBe('Tabs')
56
+ })
57
+ })
58
+
59
+ describe('Orientation', () => {
60
+ it('renders horizontal orientation by default', () => {
61
+ render(
62
+ <Tabs data-testid="tabs">
63
+ <TabsList>
64
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
65
+ </TabsList>
66
+ </Tabs>
67
+ )
68
+
69
+ const tabs = screen.getByTestId('tabs')
70
+ expect(tabs).toHaveAttribute('data-orientation', 'horizontal')
71
+ })
72
+
73
+ it('renders vertical orientation when vertical prop is true', () => {
74
+ render(
75
+ <Tabs vertical data-testid="tabs">
76
+ <TabsList>
77
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
78
+ </TabsList>
79
+ </Tabs>
80
+ )
81
+
82
+ const tabs = screen.getByTestId('tabs')
83
+ expect(tabs).toHaveAttribute('data-orientation', 'vertical')
84
+ })
85
+ })
86
+
87
+ describe('HTML Attributes', () => {
88
+ it('passes through HTML attributes', () => {
89
+ render(
90
+ <Tabs data-testid="tabs" aria-label="Navigation tabs">
91
+ <TabsList>
92
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
93
+ </TabsList>
94
+ </Tabs>
95
+ )
96
+
97
+ const tabs = screen.getByTestId('tabs')
98
+ expect(tabs).toHaveAttribute('aria-label', 'Navigation tabs')
99
+ })
100
+ })
101
+ })
102
+
103
+ describe('TabsList Component', () => {
104
+ describe('Basic Rendering', () => {
105
+ it('renders correctly with default props', () => {
106
+ render(
107
+ <Tabs>
108
+ <TabsList data-testid="tabs-list">
109
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
110
+ </TabsList>
111
+ </Tabs>
112
+ )
113
+
114
+ const tabsList = screen.getByTestId('tabs-list')
115
+ expect(tabsList).toBeInTheDocument()
116
+ expect(tabsList).toHaveAttribute('role', 'tablist')
117
+ })
118
+
119
+ it('applies custom className', () => {
120
+ render(
121
+ <Tabs>
122
+ <TabsList className="custom-list" data-testid="tabs-list">
123
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
124
+ </TabsList>
125
+ </Tabs>
126
+ )
127
+
128
+ const tabsList = screen.getByTestId('tabs-list')
129
+ expect(tabsList).toHaveClass('custom-list')
130
+ })
131
+
132
+ it('forwards ref correctly', () => {
133
+ const ref = React.createRef<HTMLDivElement>()
134
+ render(
135
+ <Tabs>
136
+ <TabsList ref={ref} data-testid="tabs-list">
137
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
138
+ </TabsList>
139
+ </Tabs>
140
+ )
141
+
142
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
143
+ })
144
+
145
+ it('maintains displayName', () => {
146
+ expect(TabsList.displayName).toBe('TabsList')
147
+ })
148
+ })
149
+
150
+ describe('Variants', () => {
151
+ it('renders default variant correctly', () => {
152
+ render(
153
+ <Tabs>
154
+ <TabsList variant="default" data-testid="tabs-list">
155
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
156
+ </TabsList>
157
+ </Tabs>
158
+ )
159
+
160
+ const tabsList = screen.getByTestId('tabs-list')
161
+ expect(tabsList).toHaveClass('bg-muted', 'rounded-md', 'p-1')
162
+ })
163
+
164
+ it('renders pills variant correctly', () => {
165
+ render(
166
+ <Tabs>
167
+ <TabsList variant="pills" data-testid="tabs-list">
168
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
169
+ </TabsList>
170
+ </Tabs>
171
+ )
172
+
173
+ const tabsList = screen.getByTestId('tabs-list')
174
+ expect(tabsList).toHaveClass('bg-transparent', 'gap-2', 'p-0')
175
+ })
176
+
177
+ it('renders underline variant correctly', () => {
178
+ render(
179
+ <Tabs>
180
+ <TabsList variant="underline" data-testid="tabs-list">
181
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
182
+ </TabsList>
183
+ </Tabs>
184
+ )
185
+
186
+ const tabsList = screen.getByTestId('tabs-list')
187
+ expect(tabsList).toHaveClass('bg-transparent', 'border-b', 'gap-4')
188
+ })
189
+
190
+ it('renders cards variant correctly', () => {
191
+ render(
192
+ <Tabs>
193
+ <TabsList variant="cards" data-testid="tabs-list">
194
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
195
+ </TabsList>
196
+ </Tabs>
197
+ )
198
+
199
+ const tabsList = screen.getByTestId('tabs-list')
200
+ expect(tabsList).toHaveClass('bg-transparent', 'gap-2', 'p-0')
201
+ })
202
+
203
+ it('renders minimal variant correctly', () => {
204
+ render(
205
+ <Tabs>
206
+ <TabsList variant="minimal" data-testid="tabs-list">
207
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
208
+ </TabsList>
209
+ </Tabs>
210
+ )
211
+
212
+ const tabsList = screen.getByTestId('tabs-list')
213
+ expect(tabsList).toHaveClass('bg-transparent', 'gap-1', 'p-0')
214
+ })
215
+ })
216
+
217
+ describe('Orientation', () => {
218
+ it('renders horizontal orientation by default', () => {
219
+ render(
220
+ <Tabs>
221
+ <TabsList data-testid="tabs-list">
222
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
223
+ </TabsList>
224
+ </Tabs>
225
+ )
226
+
227
+ const tabsList = screen.getByTestId('tabs-list')
228
+ expect(tabsList).toHaveClass('flex-row')
229
+ })
230
+
231
+ it('renders vertical orientation correctly', () => {
232
+ render(
233
+ <Tabs>
234
+ <TabsList orientation="vertical" data-testid="tabs-list">
235
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
236
+ </TabsList>
237
+ </Tabs>
238
+ )
239
+
240
+ const tabsList = screen.getByTestId('tabs-list')
241
+ expect(tabsList).toHaveClass('flex-col', 'items-start', 'gap-1')
242
+ })
243
+ })
244
+
245
+ describe('Full Width', () => {
246
+ it('renders full width when fullWidth is true', () => {
247
+ render(
248
+ <Tabs>
249
+ <TabsList fullWidth data-testid="tabs-list">
250
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
251
+ </TabsList>
252
+ </Tabs>
253
+ )
254
+
255
+ const tabsList = screen.getByTestId('tabs-list')
256
+ expect(tabsList).toHaveClass('w-full')
257
+ })
258
+ })
259
+
260
+ describe('HTML Attributes', () => {
261
+ it('passes through HTML attributes', () => {
262
+ render(
263
+ <Tabs>
264
+ <TabsList data-testid="tabs-list" aria-label="Tab navigation">
265
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
266
+ </TabsList>
267
+ </Tabs>
268
+ )
269
+
270
+ const tabsList = screen.getByTestId('tabs-list')
271
+ expect(tabsList).toHaveAttribute('aria-label', 'Tab navigation')
272
+ })
273
+ })
274
+ })
275
+
276
+ describe('TabsTrigger Component', () => {
277
+ describe('Basic Rendering', () => {
278
+ it('renders correctly with default props', () => {
279
+ render(
280
+ <Tabs>
281
+ <TabsList>
282
+ <TabsTrigger value="tab1" data-testid="tabs-trigger">Tab 1</TabsTrigger>
283
+ </TabsList>
284
+ </Tabs>
285
+ )
286
+
287
+ const trigger = screen.getByTestId('tabs-trigger')
288
+ expect(trigger).toBeInTheDocument()
289
+ expect(trigger).toHaveAttribute('role', 'tab')
290
+ expect(trigger).toHaveTextContent('Tab 1')
291
+ })
292
+
293
+ it('applies custom className', () => {
294
+ render(
295
+ <Tabs>
296
+ <TabsList>
297
+ <TabsTrigger value="tab1" className="custom-trigger" data-testid="tabs-trigger">
298
+ Tab 1
299
+ </TabsTrigger>
300
+ </TabsList>
301
+ </Tabs>
302
+ )
303
+
304
+ const trigger = screen.getByTestId('tabs-trigger')
305
+ expect(trigger).toHaveClass('custom-trigger')
306
+ })
307
+
308
+ it('forwards ref correctly', () => {
309
+ const ref = React.createRef<HTMLButtonElement>()
310
+ render(
311
+ <Tabs>
312
+ <TabsList>
313
+ <TabsTrigger value="tab1" ref={ref} data-testid="tabs-trigger">
314
+ Tab 1
315
+ </TabsTrigger>
316
+ </TabsList>
317
+ </Tabs>
318
+ )
319
+
320
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement)
321
+ })
322
+
323
+ it('maintains displayName', () => {
324
+ expect(TabsTrigger.displayName).toBe('TabsTrigger')
325
+ })
326
+ })
327
+
328
+ describe('Variants', () => {
329
+ it('renders default variant correctly', () => {
330
+ render(
331
+ <Tabs>
332
+ <TabsList>
333
+ <TabsTrigger value="tab1" variant="default" data-testid="tabs-trigger">
334
+ Tab 1
335
+ </TabsTrigger>
336
+ </TabsList>
337
+ </Tabs>
338
+ )
339
+
340
+ const trigger = screen.getByTestId('tabs-trigger')
341
+ expect(trigger).toHaveClass('rounded-md')
342
+ })
343
+
344
+ it('renders underline variant correctly', () => {
345
+ render(
346
+ <Tabs>
347
+ <TabsList>
348
+ <TabsTrigger value="tab1" variant="underline" data-testid="tabs-trigger">
349
+ Tab 1
350
+ </TabsTrigger>
351
+ </TabsList>
352
+ </Tabs>
353
+ )
354
+
355
+ const trigger = screen.getByTestId('tabs-trigger')
356
+ expect(trigger).toHaveClass('rounded-none')
357
+ expect(trigger).toHaveClass('border-b-2')
358
+ expect(trigger).toHaveClass('border-transparent')
359
+ })
360
+
361
+ it('renders pills variant correctly', () => {
362
+ render(
363
+ <Tabs>
364
+ <TabsList>
365
+ <TabsTrigger value="tab1" variant="pills" data-testid="tabs-trigger">
366
+ Tab 1
367
+ </TabsTrigger>
368
+ </TabsList>
369
+ </Tabs>
370
+ )
371
+
372
+ const trigger = screen.getByTestId('tabs-trigger')
373
+ expect(trigger).toHaveClass('rounded-full', 'bg-muted')
374
+ })
375
+
376
+ it('renders cards variant correctly', () => {
377
+ render(
378
+ <Tabs>
379
+ <TabsList>
380
+ <TabsTrigger value="tab1" variant="cards" data-testid="tabs-trigger">
381
+ Tab 1
382
+ </TabsTrigger>
383
+ </TabsList>
384
+ </Tabs>
385
+ )
386
+
387
+ const trigger = screen.getByTestId('tabs-trigger')
388
+ expect(trigger).toHaveClass('rounded-md', 'bg-muted/50')
389
+ })
390
+
391
+ it('renders minimal variant correctly', () => {
392
+ render(
393
+ <Tabs>
394
+ <TabsList>
395
+ <TabsTrigger value="tab1" variant="minimal" data-testid="tabs-trigger">
396
+ Tab 1
397
+ </TabsTrigger>
398
+ </TabsList>
399
+ </Tabs>
400
+ )
401
+
402
+ const trigger = screen.getByTestId('tabs-trigger')
403
+ expect(trigger).toHaveClass('rounded-sm', 'bg-transparent')
404
+ })
405
+ })
406
+
407
+ describe('Sizes', () => {
408
+ it('renders sm size correctly', () => {
409
+ render(
410
+ <Tabs>
411
+ <TabsList>
412
+ <TabsTrigger value="tab1" size="sm" data-testid="tabs-trigger">
413
+ Tab 1
414
+ </TabsTrigger>
415
+ </TabsList>
416
+ </Tabs>
417
+ )
418
+
419
+ const trigger = screen.getByTestId('tabs-trigger')
420
+ expect(trigger).toHaveClass('h-7', 'px-2', 'text-xs')
421
+ })
422
+
423
+ it('renders md size correctly', () => {
424
+ render(
425
+ <Tabs>
426
+ <TabsList>
427
+ <TabsTrigger value="tab1" size="md" data-testid="tabs-trigger">
428
+ Tab 1
429
+ </TabsTrigger>
430
+ </TabsList>
431
+ </Tabs>
432
+ )
433
+
434
+ const trigger = screen.getByTestId('tabs-trigger')
435
+ expect(trigger).toHaveClass('h-9', 'px-3', 'text-sm')
436
+ })
437
+
438
+ it('renders lg size correctly', () => {
439
+ render(
440
+ <Tabs>
441
+ <TabsList>
442
+ <TabsTrigger value="tab1" size="lg" data-testid="tabs-trigger">
443
+ Tab 1
444
+ </TabsTrigger>
445
+ </TabsList>
446
+ </Tabs>
447
+ )
448
+
449
+ const trigger = screen.getByTestId('tabs-trigger')
450
+ expect(trigger).toHaveClass('h-10', 'px-4', 'text-base')
451
+ })
452
+ })
453
+
454
+ describe('Icons', () => {
455
+ it('renders left icon correctly', () => {
456
+ render(
457
+ <Tabs>
458
+ <TabsList>
459
+ <TabsTrigger
460
+ value="tab1"
461
+ icon={<MockIcon />}
462
+ iconPosition="left"
463
+ data-testid="tabs-trigger"
464
+ >
465
+ Tab 1
466
+ </TabsTrigger>
467
+ </TabsList>
468
+ </Tabs>
469
+ )
470
+
471
+ const trigger = screen.getByTestId('tabs-trigger')
472
+ const icon = screen.getByTestId('mock-icon')
473
+ expect(icon).toBeInTheDocument()
474
+ expect(icon.parentElement).toHaveClass('mr-2')
475
+ })
476
+
477
+ it('renders right icon correctly', () => {
478
+ render(
479
+ <Tabs>
480
+ <TabsList>
481
+ <TabsTrigger
482
+ value="tab1"
483
+ icon={<MockIcon />}
484
+ iconPosition="right"
485
+ data-testid="tabs-trigger"
486
+ >
487
+ Tab 1
488
+ </TabsTrigger>
489
+ </TabsList>
490
+ </Tabs>
491
+ )
492
+
493
+ const trigger = screen.getByTestId('tabs-trigger')
494
+ const icon = screen.getByTestId('mock-icon')
495
+ expect(icon).toBeInTheDocument()
496
+ expect(icon.parentElement).toHaveClass('ml-2')
497
+ })
498
+
499
+ it('does not render icon when iconPosition is none', () => {
500
+ render(
501
+ <Tabs>
502
+ <TabsList>
503
+ <TabsTrigger
504
+ value="tab1"
505
+ icon={<MockIcon />}
506
+ iconPosition="none"
507
+ data-testid="tabs-trigger"
508
+ >
509
+ Tab 1
510
+ </TabsTrigger>
511
+ </TabsList>
512
+ </Tabs>
513
+ )
514
+
515
+ expect(screen.queryByTestId('mock-icon')).not.toBeInTheDocument()
516
+ })
517
+ })
518
+
519
+ describe('Badge', () => {
520
+ it('renders badge correctly', () => {
521
+ render(
522
+ <Tabs>
523
+ <TabsList>
524
+ <TabsTrigger
525
+ value="tab1"
526
+ badge={<MockBadge />}
527
+ data-testid="tabs-trigger"
528
+ >
529
+ Tab 1
530
+ </TabsTrigger>
531
+ </TabsList>
532
+ </Tabs>
533
+ )
534
+
535
+ const badge = screen.getByTestId('mock-badge')
536
+ expect(badge).toBeInTheDocument()
537
+ expect(badge.parentElement).toHaveClass('ml-2')
538
+ })
539
+ })
540
+
541
+ describe('Fade Tabs', () => {
542
+ it('applies fade class when fadeTabs is true', () => {
543
+ render(
544
+ <Tabs>
545
+ <TabsList>
546
+ <TabsTrigger value="tab1" fadeTabs data-testid="tabs-trigger">
547
+ Tab 1
548
+ </TabsTrigger>
549
+ </TabsList>
550
+ </Tabs>
551
+ )
552
+
553
+ const trigger = screen.getByTestId('tabs-trigger')
554
+ expect(trigger).toHaveClass('data-[state=inactive]:opacity-60')
555
+ })
556
+ })
557
+
558
+ describe('Orientation', () => {
559
+ it('renders vertical orientation correctly', () => {
560
+ render(
561
+ <Tabs>
562
+ <TabsList>
563
+ <TabsTrigger value="tab1" orientation="vertical" data-testid="tabs-trigger">
564
+ Tab 1
565
+ </TabsTrigger>
566
+ </TabsList>
567
+ </Tabs>
568
+ )
569
+
570
+ const trigger = screen.getByTestId('tabs-trigger')
571
+ expect(trigger).toHaveClass('justify-start', 'w-full', 'text-left')
572
+ })
573
+ })
574
+
575
+ describe('Full Width', () => {
576
+ it('renders full width when fullWidth is true', () => {
577
+ render(
578
+ <Tabs>
579
+ <TabsList>
580
+ <TabsTrigger value="tab1" fullWidth data-testid="tabs-trigger">
581
+ Tab 1
582
+ </TabsTrigger>
583
+ </TabsList>
584
+ </Tabs>
585
+ )
586
+
587
+ const trigger = screen.getByTestId('tabs-trigger')
588
+ expect(trigger).toHaveClass('w-full')
589
+ })
590
+ })
591
+
592
+ describe('Interactions', () => {
593
+ it('handles click events', async () => {
594
+ const onValueChange = jest.fn()
595
+ const user = userEvent.setup()
596
+
597
+ render(
598
+ <Tabs onValueChange={onValueChange} defaultValue="tab2">
599
+ <TabsList>
600
+ <TabsTrigger value="tab1" data-testid="tabs-trigger">
601
+ Tab 1
602
+ </TabsTrigger>
603
+ <TabsTrigger value="tab2" data-testid="tabs-trigger-2">
604
+ Tab 2
605
+ </TabsTrigger>
606
+ </TabsList>
607
+ <TabsContent value="tab1">Content 1</TabsContent>
608
+ <TabsContent value="tab2">Content 2</TabsContent>
609
+ </Tabs>
610
+ )
611
+
612
+ // Verify initial state - tab2 should be active
613
+ const trigger1 = screen.getByTestId('tabs-trigger')
614
+ const trigger2 = screen.getByTestId('tabs-trigger-2')
615
+
616
+ expect(trigger2).toHaveAttribute('data-state', 'active')
617
+ expect(trigger1).toHaveAttribute('data-state', 'inactive')
618
+
619
+ // Click on tab1 while tab2 is active (should trigger onValueChange)
620
+ await user.click(trigger1)
621
+
622
+ // Wait for callback to be called
623
+ await waitFor(() => {
624
+ expect(onValueChange).toHaveBeenCalledWith('tab1')
625
+ })
626
+ })
627
+ })
628
+
629
+ describe('HTML Attributes', () => {
630
+ it('passes through HTML attributes', () => {
631
+ render(
632
+ <Tabs>
633
+ <TabsList>
634
+ <TabsTrigger value="tab1" data-testid="tabs-trigger" aria-label="First tab">
635
+ Tab 1
636
+ </TabsTrigger>
637
+ </TabsList>
638
+ </Tabs>
639
+ )
640
+
641
+ const trigger = screen.getByTestId('tabs-trigger')
642
+ expect(trigger).toHaveAttribute('aria-label', 'First tab')
643
+ })
644
+ })
645
+ })
646
+
647
+ describe('TabsContent Component', () => {
648
+ describe('Basic Rendering', () => {
649
+ it('renders correctly with default props', () => {
650
+ render(
651
+ <Tabs defaultValue="tab1">
652
+ <TabsList>
653
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
654
+ </TabsList>
655
+ <TabsContent value="tab1" data-testid="tabs-content">
656
+ Content 1
657
+ </TabsContent>
658
+ </Tabs>
659
+ )
660
+
661
+ const content = screen.getByTestId('tabs-content')
662
+ expect(content).toBeInTheDocument()
663
+ expect(content).toHaveTextContent('Content 1')
664
+ expect(content).toHaveAttribute('role', 'tabpanel')
665
+ })
666
+
667
+ it('applies custom className', () => {
668
+ render(
669
+ <Tabs defaultValue="tab1">
670
+ <TabsList>
671
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
672
+ </TabsList>
673
+ <TabsContent value="tab1" className="custom-content" data-testid="tabs-content">
674
+ Content 1
675
+ </TabsContent>
676
+ </Tabs>
677
+ )
678
+
679
+ const content = screen.getByTestId('tabs-content')
680
+ expect(content).toHaveClass('custom-content')
681
+ })
682
+
683
+ it('forwards ref correctly', () => {
684
+ const ref = React.createRef<HTMLDivElement>()
685
+ render(
686
+ <Tabs defaultValue="tab1">
687
+ <TabsList>
688
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
689
+ </TabsList>
690
+ <TabsContent value="tab1" ref={ref} data-testid="tabs-content">
691
+ Content 1
692
+ </TabsContent>
693
+ </Tabs>
694
+ )
695
+
696
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
697
+ })
698
+
699
+ it('maintains displayName', () => {
700
+ expect(TabsContent.displayName).toBe('TabsContent')
701
+ })
702
+ })
703
+
704
+ describe('Animation', () => {
705
+ it('applies animation classes when animated is true', () => {
706
+ render(
707
+ <Tabs defaultValue="tab1">
708
+ <TabsList>
709
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
710
+ </TabsList>
711
+ <TabsContent value="tab1" animated data-testid="tabs-content">
712
+ Content 1
713
+ </TabsContent>
714
+ </Tabs>
715
+ )
716
+
717
+ const content = screen.getByTestId('tabs-content')
718
+ expect(content).toHaveClass('data-[state=active]:animate-fadeIn', 'data-[state=inactive]:animate-fadeOut')
719
+ })
720
+ })
721
+
722
+ describe('HTML Attributes', () => {
723
+ it('passes through HTML attributes', () => {
724
+ render(
725
+ <Tabs defaultValue="tab1">
726
+ <TabsList>
727
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
728
+ </TabsList>
729
+ <TabsContent value="tab1" data-testid="tabs-content" aria-label="Tab content">
730
+ Content 1
731
+ </TabsContent>
732
+ </Tabs>
733
+ )
734
+
735
+ const content = screen.getByTestId('tabs-content')
736
+ expect(content).toHaveAttribute('aria-label', 'Tab content')
737
+ })
738
+ })
739
+ })
740
+
741
+ describe('Complex Combinations', () => {
742
+ it('renders complete tabs with all components', () => {
743
+ render(
744
+ <Tabs defaultValue="tab1" data-testid="tabs">
745
+ <TabsList variant="pills" data-testid="tabs-list">
746
+ <TabsTrigger
747
+ value="tab1"
748
+ size="lg"
749
+ icon={<MockIcon />}
750
+ badge={<MockBadge />}
751
+ data-testid="tabs-trigger-1"
752
+ >
753
+ Tab 1
754
+ </TabsTrigger>
755
+ <TabsTrigger value="tab2" data-testid="tabs-trigger-2">
756
+ Tab 2
757
+ </TabsTrigger>
758
+ </TabsList>
759
+ <TabsContent value="tab1" animated data-testid="tabs-content-1">
760
+ Content 1
761
+ </TabsContent>
762
+ <TabsContent value="tab2" data-testid="tabs-content-2">
763
+ Content 2
764
+ </TabsContent>
765
+ </Tabs>
766
+ )
767
+
768
+ expect(screen.getByTestId('tabs')).toBeInTheDocument()
769
+ expect(screen.getByTestId('tabs-list')).toHaveClass('bg-transparent', 'gap-2')
770
+ expect(screen.getByTestId('tabs-trigger-1')).toHaveClass('h-10', 'px-4')
771
+ expect(screen.getByTestId('mock-icon')).toBeInTheDocument()
772
+ expect(screen.getByTestId('mock-badge')).toBeInTheDocument()
773
+ expect(screen.getByTestId('tabs-content-1')).toBeInTheDocument()
774
+ })
775
+
776
+ it('handles vertical tabs with all variants', () => {
777
+ render(
778
+ <Tabs vertical defaultValue="tab1" data-testid="tabs">
779
+ <TabsList orientation="vertical" variant="underline" data-testid="tabs-list">
780
+ <TabsTrigger
781
+ value="tab1"
782
+ orientation="vertical"
783
+ variant="underline"
784
+ fadeTabs
785
+ data-testid="tabs-trigger"
786
+ >
787
+ Tab 1
788
+ </TabsTrigger>
789
+ </TabsList>
790
+ <TabsContent value="tab1" animated data-testid="tabs-content">
791
+ Content 1
792
+ </TabsContent>
793
+ </Tabs>
794
+ )
795
+
796
+ const tabs = screen.getByTestId('tabs')
797
+ const tabsList = screen.getByTestId('tabs-list')
798
+ const trigger = screen.getByTestId('tabs-trigger')
799
+
800
+ expect(tabs).toHaveAttribute('data-orientation', 'vertical')
801
+ expect(tabsList).toHaveClass('flex-col', 'items-start')
802
+ expect(trigger).toHaveClass('justify-start', 'w-full', 'data-[state=inactive]:opacity-60')
803
+ })
804
+
805
+ it('handles tab switching', () => {
806
+ render(
807
+ <Tabs defaultValue="tab1">
808
+ <TabsList>
809
+ <TabsTrigger value="tab1" data-testid="trigger-1">Tab 1</TabsTrigger>
810
+ <TabsTrigger value="tab2" data-testid="trigger-2">Tab 2</TabsTrigger>
811
+ </TabsList>
812
+ <TabsContent value="tab1" data-testid="content-1">Content 1</TabsContent>
813
+ <TabsContent value="tab2" data-testid="content-2">Content 2</TabsContent>
814
+ </Tabs>
815
+ )
816
+
817
+ // Initially tab1 should be active
818
+ expect(screen.getByTestId('content-1')).toBeInTheDocument()
819
+
820
+ // Click tab2
821
+ fireEvent.click(screen.getByTestId('trigger-2'))
822
+
823
+ // Now tab2 content should be visible
824
+ expect(screen.getByTestId('content-2')).toBeInTheDocument()
825
+ })
826
+ })
827
+
828
+ describe('Edge Cases', () => {
829
+ it('handles empty tabs', () => {
830
+ render(<Tabs data-testid="tabs" />)
831
+
832
+ const tabs = screen.getByTestId('tabs')
833
+ expect(tabs).toBeInTheDocument()
834
+ })
835
+
836
+ it('handles tabs without content', () => {
837
+ render(
838
+ <Tabs>
839
+ <TabsList>
840
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
841
+ </TabsList>
842
+ </Tabs>
843
+ )
844
+
845
+ const trigger = screen.getByRole('tab')
846
+ expect(trigger).toBeInTheDocument()
847
+ })
848
+
849
+ it('handles complex nested content', () => {
850
+ render(
851
+ <Tabs defaultValue="tab1">
852
+ <TabsList>
853
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
854
+ </TabsList>
855
+ <TabsContent value="tab1" data-testid="tabs-content">
856
+ <div>
857
+ <h2>Nested Title</h2>
858
+ <p>Nested paragraph</p>
859
+ <button>Nested button</button>
860
+ </div>
861
+ </TabsContent>
862
+ </Tabs>
863
+ )
864
+
865
+ const content = screen.getByTestId('tabs-content')
866
+ expect(content).toHaveTextContent('Nested Title')
867
+ expect(content).toHaveTextContent('Nested paragraph')
868
+ expect(screen.getByRole('button', { name: 'Nested button' })).toBeInTheDocument()
869
+ })
870
+
871
+ it('passes through HTML attributes for all components', () => {
872
+ render(
873
+ <Tabs data-custom="tabs" defaultValue="tab1">
874
+ <TabsList data-custom="list">
875
+ <TabsTrigger value="tab1" data-custom="trigger">Tab 1</TabsTrigger>
876
+ </TabsList>
877
+ <TabsContent value="tab1" data-custom="content">Content 1</TabsContent>
878
+ </Tabs>
879
+ )
880
+
881
+ expect(screen.getByRole('tablist').closest('[data-custom="tabs"]')).toBeInTheDocument()
882
+ expect(screen.getByRole('tablist')).toHaveAttribute('data-custom', 'list')
883
+ expect(screen.getByRole('tab')).toHaveAttribute('data-custom', 'trigger')
884
+ expect(screen.getByRole('tabpanel')).toHaveAttribute('data-custom', 'content')
885
+ })
886
+ })
887
+ })