@moontra/moonui 5.0.0 → 5.0.1

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 (33) hide show
  1. package/dist/index.d.mts +2 -2
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.global.js +9 -9
  4. package/dist/index.global.js.map +1 -1
  5. package/dist/index.js +17 -11
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +17 -11
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/lib/theme.global.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/__tests__/use-intersection-observer.test.tsx +18 -4
  12. package/src/__tests__/use-local-storage.test.tsx +34 -20
  13. package/src/components/ui/__tests__/avatar.test.tsx +5 -7
  14. package/src/components/ui/__tests__/badge.test.tsx +19 -12
  15. package/src/components/ui/__tests__/breadcrumb.test.tsx +4 -7
  16. package/src/components/ui/__tests__/button.test.tsx +6 -3
  17. package/src/components/ui/__tests__/card.test.tsx +4 -1
  18. package/src/components/ui/__tests__/checkbox.test.tsx +4 -1
  19. package/src/components/ui/__tests__/command.test.tsx +268 -124
  20. package/src/components/ui/__tests__/data-table-basic.test.tsx +271 -0
  21. package/src/components/ui/__tests__/date-picker.test.tsx +184 -197
  22. package/src/components/ui/__tests__/progress.test.tsx +0 -1
  23. package/src/components/ui/__tests__/table.test.tsx +8 -2
  24. package/src/components/ui/__tests__/tabs.test.tsx +5 -3
  25. package/src/components/ui/__tests__/toast.test.tsx +461 -159
  26. package/src/components/ui/__tests__/tooltip.test.tsx +387 -264
  27. package/src/components/ui/command.tsx +11 -7
  28. package/src/components/ui/date-picker.tsx +12 -6
  29. package/src/components/ui/toast.tsx +3 -0
  30. package/src/use-intersection-observer.tsx +9 -0
  31. package/src/use-local-storage.tsx +35 -1
  32. package/src/components/ui/__tests__/data-table.test.tsx +0 -256
  33. package/src/components/ui/data-table.stories.tsx +0 -511
@@ -12,23 +12,43 @@ import {
12
12
  CommandItem,
13
13
  CommandShortcut,
14
14
  CommandSeparator,
15
- commandVariants,
16
- commandInputVariants,
17
- commandListVariants,
18
- commandGroupVariants,
19
- commandItemVariants,
20
15
  } from '../command'
21
16
 
22
- // Mock Dialog component
23
- jest.mock('./dialog', () => ({
24
- Dialog: ({ children, open, ...props }: any) => (
25
- <div data-testid="dialog" data-open={open ? 'true' : 'false'} {...props}>{children}</div>
17
+ // Dialog mock — CommandDialog kaynakta Dialog + DialogContent + DialogTitle kullanır.
18
+ // Üçü de mock'lanmalı; eksik biri `undefined` element type hatası verir.
19
+ jest.mock('../dialog', () => ({
20
+ Dialog: ({ children, open, onOpenChange, ...props }: any) => (
21
+ <div
22
+ data-testid="dialog"
23
+ data-open={open ? 'true' : 'false'}
24
+ data-has-open-change={onOpenChange ? 'true' : 'false'}
25
+ {...props}
26
+ >
27
+ {children}
28
+ </div>
26
29
  ),
27
- DialogContent: ({ children, className, ...props }: React.ComponentProps<'div'>) => (
28
- <div data-testid="dialog-content" className={className} {...props}>{children}</div>
30
+ DialogContent: ({ children, className, ...props }: any) => {
31
+ // hideCloseButton gerçek DialogContent'in prop'u; mock'taki div'e sızarsa React uyarır
32
+ delete props.hideCloseButton
33
+ return <div data-testid="dialog-content" className={className} {...props}>{children}</div>
34
+ },
35
+ DialogTitle: ({ children, className, ...props }: any) => (
36
+ <div data-testid="dialog-title" className={className} {...props}>{children}</div>
29
37
  ),
30
38
  }))
31
39
 
40
+ // jsdom `Element.prototype.scrollIntoView` implement etmez; cmdk bir item seçildiğinde
41
+ // bunu çağırır. Polyfill olmadan her item render'ı TypeError ile patlar.
42
+ beforeAll(() => {
43
+ ;(Element.prototype as any).scrollIntoView = jest.fn()
44
+ })
45
+
46
+ // cmdk alt bileşenleri (Input/List/Empty/Group/Item/Separator) Command kökündeki context
47
+ // store'a bağlıdır; kök olmadan render edilirlerse "Cannot read properties of undefined
48
+ // (reading 'subscribe')" ile patlar. Tüm alt bileşen testleri bu yardımcı ile sarılır.
49
+ const renderInCommand = (ui: React.ReactNode, commandProps: Record<string, unknown> = {}) =>
50
+ render(<Command {...commandProps}>{ui}</Command>)
51
+
32
52
  describe('Command Components', () => {
33
53
  describe('Command Component', () => {
34
54
  it('renders correctly with default props', () => {
@@ -82,21 +102,21 @@ describe('Command Components', () => {
82
102
  render(<Command size="sm" data-testid="command">Content</Command>)
83
103
 
84
104
  const command = screen.getByTestId('command')
85
- expect(command).toHaveClass('p-2')
105
+ expect(command).toHaveClass('min-h-[200px]')
86
106
  })
87
107
 
88
108
  it('renders default size correctly', () => {
89
109
  render(<Command data-testid="command">Content</Command>)
90
110
 
91
111
  const command = screen.getByTestId('command')
92
- expect(command).toHaveClass('p-4')
112
+ expect(command).toHaveClass('min-h-[300px]')
93
113
  })
94
114
 
95
115
  it('renders lg size correctly', () => {
96
116
  render(<Command size="lg" data-testid="command">Content</Command>)
97
117
 
98
118
  const command = screen.getByTestId('command')
99
- expect(command).toHaveClass('p-6')
119
+ expect(command).toHaveClass('min-h-[400px]')
100
120
  })
101
121
 
102
122
  it('passes through HTML attributes', () => {
@@ -119,6 +139,9 @@ describe('Command Components', () => {
119
139
  const dialogContent = screen.getByTestId('dialog-content')
120
140
  expect(dialog).toBeInTheDocument()
121
141
  expect(dialogContent).toBeInTheDocument()
142
+ // Erişilebilirlik: dialog'un sr-only başlığı her zaman render edilir
143
+ expect(screen.getByTestId('dialog-title')).toHaveTextContent('Command Menu')
144
+ expect(screen.getByText('Dialog Content')).toBeInTheDocument()
122
145
  })
123
146
 
124
147
  it('applies custom commandClassName', () => {
@@ -142,12 +165,14 @@ describe('Command Components', () => {
142
165
 
143
166
  const dialog = screen.getByTestId('dialog')
144
167
  expect(dialog).toHaveAttribute('data-open', 'true')
168
+ // onOpenChange alttaki Dialog'a iletilmeli (CommandDialog kendisi tüketmemeli)
169
+ expect(dialog).toHaveAttribute('data-has-open-change', 'true')
145
170
  })
146
171
  })
147
172
 
148
173
  describe('CommandInput Component', () => {
149
174
  it('renders correctly with default props', () => {
150
- render(<CommandInput data-testid="command-input" />)
175
+ renderInCommand(<CommandInput data-testid="command-input" />)
151
176
 
152
177
  const input = screen.getByTestId('command-input')
153
178
  expect(input).toBeInTheDocument()
@@ -157,7 +182,7 @@ describe('Command Components', () => {
157
182
  })
158
183
 
159
184
  it('renders search icon', () => {
160
- render(<CommandInput />)
185
+ renderInCommand(<CommandInput />)
161
186
 
162
187
  const searchIcon = document.querySelector('svg')
163
188
  expect(searchIcon).toBeInTheDocument()
@@ -165,7 +190,7 @@ describe('Command Components', () => {
165
190
  })
166
191
 
167
192
  it('applies custom className', () => {
168
- render(<CommandInput className="custom-input" data-testid="command-input" />)
193
+ renderInCommand(<CommandInput className="custom-input" data-testid="command-input" />)
169
194
 
170
195
  const input = screen.getByTestId('command-input')
171
196
  expect(input).toHaveClass('custom-input')
@@ -173,7 +198,7 @@ describe('Command Components', () => {
173
198
 
174
199
  it('forwards ref correctly', () => {
175
200
  const ref = React.createRef<HTMLInputElement>()
176
- render(<CommandInput ref={ref} />)
201
+ renderInCommand(<CommandInput ref={ref} />)
177
202
 
178
203
  expect(ref.current).toBeInstanceOf(HTMLInputElement)
179
204
  })
@@ -182,41 +207,61 @@ describe('Command Components', () => {
182
207
  expect(CommandInput.displayName).toBe('CommandInput')
183
208
  })
184
209
 
185
- it('renders minimal variant correctly', () => {
186
- render(<CommandInput variant="minimal" data-testid="command-input" />)
210
+ it('renders inside the cmdk input wrapper', () => {
211
+ renderInCommand(<CommandInput data-testid="command-input" />)
187
212
 
188
213
  const input = screen.getByTestId('command-input')
189
- expect(input).toHaveClass('h-9')
190
- })
191
-
192
- it('renders bordered variant correctly', () => {
193
- render(<CommandInput variant="bordered" data-testid="command-input" />)
214
+ const wrapper = input.closest('[cmdk-input-wrapper]')
215
+ expect(wrapper).toBeInTheDocument()
216
+ expect(wrapper).toHaveClass('flex items-center border-b px-3')
217
+ // Arama ikonu wrapper içinde, input'tan önce yer alır
218
+ expect(wrapper?.querySelector('svg')).toBeInTheDocument()
219
+ })
220
+
221
+ it('exposes combobox ARIA wiring', () => {
222
+ renderInCommand(
223
+ <>
224
+ <CommandInput data-testid="command-input" />
225
+ <CommandList data-testid="command-list" />
226
+ </>
227
+ )
194
228
 
195
229
  const input = screen.getByTestId('command-input')
196
- expect(input).toHaveClass('border-b border-border')
230
+ expect(input).toHaveAttribute('role', 'combobox')
231
+ expect(input).toHaveAttribute('aria-autocomplete', 'list')
232
+ expect(input).toHaveAttribute('aria-expanded', 'true')
233
+ // aria-controls, listenin id'sini işaret etmeli
234
+ expect(input).toHaveAttribute('aria-controls', screen.getByTestId('command-list').id)
197
235
  })
198
236
 
199
237
  it('handles input events', () => {
200
- const onChange = jest.fn()
201
- render(<CommandInput onChange={onChange} />)
238
+ // cmdk input'u kontrollüdür ve kendi onChange'ini bağlar; tüketici arama değerini
239
+ // `onValueChange` ile alır.
240
+ const onValueChange = jest.fn()
241
+ renderInCommand(<CommandInput onValueChange={onValueChange} data-testid="command-input" />)
202
242
 
203
- const input = screen.getByRole('textbox')
243
+ const input = screen.getByTestId('command-input')
204
244
  fireEvent.change(input, { target: { value: 'test' } })
205
- expect(onChange).toHaveBeenCalled()
245
+
246
+ expect(onValueChange).toHaveBeenCalledWith('test')
247
+ expect(input).toHaveValue('test')
206
248
  })
207
249
 
208
250
  it('passes through HTML attributes', () => {
209
- render(<CommandInput placeholder="Search..." id="search-input" />)
251
+ // NOT: `id` cmdk tarafından label eşleşmesi için üretilir ve override edilir;
252
+ // diğer HTML attribute'ları olduğu gibi input'a geçer.
253
+ renderInCommand(<CommandInput placeholder="Search..." name="search-input" maxLength={20} />)
210
254
 
211
- const input = screen.getByRole('textbox')
255
+ const input = screen.getByRole('combobox')
212
256
  expect(input).toHaveAttribute('placeholder', 'Search...')
213
- expect(input).toHaveAttribute('id', 'search-input')
257
+ expect(input).toHaveAttribute('name', 'search-input')
258
+ expect(input).toHaveAttribute('maxlength', '20')
214
259
  })
215
260
  })
216
261
 
217
262
  describe('CommandList Component', () => {
218
263
  it('renders correctly with default props', () => {
219
- render(<CommandList data-testid="command-list">List Content</CommandList>)
264
+ renderInCommand(<CommandList data-testid="command-list">List Content</CommandList>)
220
265
 
221
266
  const list = screen.getByTestId('command-list')
222
267
  expect(list).toBeInTheDocument()
@@ -225,7 +270,7 @@ describe('Command Components', () => {
225
270
  })
226
271
 
227
272
  it('applies custom className', () => {
228
- render(<CommandList className="custom-list" data-testid="command-list">Content</CommandList>)
273
+ renderInCommand(<CommandList className="custom-list" data-testid="command-list">Content</CommandList>)
229
274
 
230
275
  const list = screen.getByTestId('command-list')
231
276
  expect(list).toHaveClass('custom-list')
@@ -233,7 +278,7 @@ describe('Command Components', () => {
233
278
 
234
279
  it('forwards ref correctly', () => {
235
280
  const ref = React.createRef<HTMLDivElement>()
236
- render(<CommandList ref={ref}>Content</CommandList>)
281
+ renderInCommand(<CommandList ref={ref}>Content</CommandList>)
237
282
 
238
283
  expect(ref.current).toBeInstanceOf(HTMLDivElement)
239
284
  })
@@ -242,31 +287,41 @@ describe('Command Components', () => {
242
287
  expect(CommandList.displayName).toBe('CommandList')
243
288
  })
244
289
 
245
- it('renders scrollable variant correctly', () => {
246
- render(<CommandList variant="scrollable" data-testid="command-list">Content</CommandList>)
290
+ it('wraps children in the cmdk list sizer', () => {
291
+ renderInCommand(<CommandList data-testid="command-list">List Content</CommandList>)
247
292
 
248
293
  const list = screen.getByTestId('command-list')
249
- expect(list).toHaveClass('max-h-[400px]')
294
+ const sizer = list.querySelector('[cmdk-list-sizer]')
295
+ expect(sizer).toBeInTheDocument()
296
+ expect(sizer).toHaveTextContent('List Content')
250
297
  })
251
298
 
252
- it('renders compact variant correctly', () => {
253
- render(<CommandList variant="compact" data-testid="command-list">Content</CommandList>)
299
+ it('uses the default accessible label and honours a custom one', () => {
300
+ const { unmount } = renderInCommand(<CommandList data-testid="command-list">Content</CommandList>)
301
+ expect(screen.getByTestId('command-list')).toHaveAttribute('aria-label', 'Suggestions')
302
+ unmount()
254
303
 
255
- const list = screen.getByTestId('command-list')
256
- expect(list).toHaveClass('max-h-[200px]')
304
+ renderInCommand(<CommandList label="Results" data-testid="command-list">Content</CommandList>)
305
+ expect(screen.getByTestId('command-list')).toHaveAttribute('aria-label', 'Results')
257
306
  })
258
307
 
259
308
  it('passes through HTML attributes', () => {
260
- render(<CommandList data-testid="command-list" id="list-1">Content</CommandList>)
309
+ // NOT: `id` cmdk tarafından input'un aria-controls'ü için üretilir; diğer
310
+ // attribute'lar olduğu gibi geçer.
311
+ renderInCommand(<CommandList data-testid="command-list" title="Command list">Content</CommandList>)
261
312
 
262
313
  const list = screen.getByTestId('command-list')
263
- expect(list).toHaveAttribute('id', 'list-1')
314
+ expect(list).toHaveAttribute('title', 'Command list')
264
315
  })
265
316
  })
266
317
 
267
318
  describe('CommandEmpty Component', () => {
268
319
  it('renders correctly with default props', () => {
269
- render(<CommandEmpty data-testid="command-empty">No results</CommandEmpty>)
320
+ renderInCommand(
321
+ <CommandList>
322
+ <CommandEmpty data-testid="command-empty">No results</CommandEmpty>
323
+ </CommandList>
324
+ )
270
325
 
271
326
  const empty = screen.getByTestId('command-empty')
272
327
  expect(empty).toBeInTheDocument()
@@ -274,7 +329,11 @@ describe('Command Components', () => {
274
329
  })
275
330
 
276
331
  it('applies custom className', () => {
277
- render(<CommandEmpty className="custom-empty" data-testid="command-empty">No results</CommandEmpty>)
332
+ renderInCommand(
333
+ <CommandList>
334
+ <CommandEmpty className="custom-empty" data-testid="command-empty">No results</CommandEmpty>
335
+ </CommandList>
336
+ )
278
337
 
279
338
  const empty = screen.getByTestId('command-empty')
280
339
  expect(empty).toHaveClass('custom-empty')
@@ -282,7 +341,11 @@ describe('Command Components', () => {
282
341
 
283
342
  it('forwards ref correctly', () => {
284
343
  const ref = React.createRef<HTMLDivElement>()
285
- render(<CommandEmpty ref={ref}>No results</CommandEmpty>)
344
+ renderInCommand(
345
+ <CommandList>
346
+ <CommandEmpty ref={ref}>No results</CommandEmpty>
347
+ </CommandList>
348
+ )
286
349
 
287
350
  expect(ref.current).toBeInstanceOf(HTMLDivElement)
288
351
  })
@@ -292,7 +355,11 @@ describe('Command Components', () => {
292
355
  })
293
356
 
294
357
  it('passes through HTML attributes', () => {
295
- render(<CommandEmpty data-testid="command-empty" id="empty-1">No results</CommandEmpty>)
358
+ renderInCommand(
359
+ <CommandList>
360
+ <CommandEmpty data-testid="command-empty" id="empty-1">No results</CommandEmpty>
361
+ </CommandList>
362
+ )
296
363
 
297
364
  const empty = screen.getByTestId('command-empty')
298
365
  expect(empty).toHaveAttribute('id', 'empty-1')
@@ -301,7 +368,7 @@ describe('Command Components', () => {
301
368
 
302
369
  describe('CommandGroup Component', () => {
303
370
  it('renders correctly with default props', () => {
304
- render(<CommandGroup data-testid="command-group">Group Content</CommandGroup>)
371
+ renderInCommand(<CommandGroup data-testid="command-group">Group Content</CommandGroup>)
305
372
 
306
373
  const group = screen.getByTestId('command-group')
307
374
  expect(group).toBeInTheDocument()
@@ -309,7 +376,7 @@ describe('Command Components', () => {
309
376
  })
310
377
 
311
378
  it('applies custom className', () => {
312
- render(<CommandGroup className="custom-group" data-testid="command-group">Content</CommandGroup>)
379
+ renderInCommand(<CommandGroup className="custom-group" data-testid="command-group">Content</CommandGroup>)
313
380
 
314
381
  const group = screen.getByTestId('command-group')
315
382
  expect(group).toHaveClass('custom-group')
@@ -317,7 +384,7 @@ describe('Command Components', () => {
317
384
 
318
385
  it('forwards ref correctly', () => {
319
386
  const ref = React.createRef<HTMLDivElement>()
320
- render(<CommandGroup ref={ref}>Content</CommandGroup>)
387
+ renderInCommand(<CommandGroup ref={ref}>Content</CommandGroup>)
321
388
 
322
389
  expect(ref.current).toBeInstanceOf(HTMLDivElement)
323
390
  })
@@ -327,40 +394,55 @@ describe('Command Components', () => {
327
394
  })
328
395
 
329
396
  it('renders heading when provided', () => {
330
- render(
397
+ renderInCommand(
331
398
  <CommandGroup heading="Group Title" data-testid="command-group">
332
399
  Group Content
333
400
  </CommandGroup>
334
401
  )
335
402
 
336
- expect(screen.getByText('Group Title')).toBeInTheDocument()
337
403
  const heading = screen.getByText('Group Title')
338
- expect(heading).toHaveClass('px-2 py-1.5 text-xs font-medium text-muted-foreground')
404
+ expect(heading).toBeInTheDocument()
405
+ expect(heading).toHaveAttribute('cmdk-group-heading', '')
406
+ // Başlık stilleri, group üzerindeki arbitrary-variant sınıflarıyla verilir
407
+ const group = screen.getByTestId('command-group')
408
+ expect(group).toHaveClass('[&_[cmdk-group-heading]]:px-2')
409
+ expect(group).toHaveClass('[&_[cmdk-group-heading]]:py-1.5')
410
+ expect(group).toHaveClass('[&_[cmdk-group-heading]]:text-xs')
411
+ expect(group).toHaveClass('[&_[cmdk-group-heading]]:font-medium')
412
+ expect(group).toHaveClass('[&_[cmdk-group-heading]]:text-muted-foreground')
339
413
  })
340
414
 
341
415
  it('renders without heading', () => {
342
- render(<CommandGroup data-testid="command-group">Group Content</CommandGroup>)
416
+ renderInCommand(<CommandGroup data-testid="command-group">Group Content</CommandGroup>)
343
417
 
344
418
  const group = screen.getByTestId('command-group')
345
- expect(group.querySelector('.px-2.py-1\\.5')).not.toBeInTheDocument()
419
+ expect(group.querySelector('[cmdk-group-heading]')).not.toBeInTheDocument()
346
420
  })
347
421
 
348
- it('renders separated variant correctly', () => {
349
- render(<CommandGroup variant="separated" data-testid="command-group">Content</CommandGroup>)
422
+ it('renders items inside a labelled group container', () => {
423
+ renderInCommand(
424
+ <CommandGroup heading="Group Title" data-testid="command-group">
425
+ <CommandItem value="a">Item A</CommandItem>
426
+ </CommandGroup>
427
+ )
350
428
 
351
429
  const group = screen.getByTestId('command-group')
352
- expect(group).toHaveClass('mt-2 border-t border-border pt-2')
430
+ const items = group.querySelector('[cmdk-group-items]')
431
+ expect(items).toBeInTheDocument()
432
+ expect(items).toHaveAttribute('role', 'group')
433
+ // Başlık, item konteynerine aria-labelledby ile bağlanır
434
+ expect(items).toHaveAttribute('aria-labelledby', screen.getByText('Group Title').id)
353
435
  })
354
436
 
355
- it('renders indented variant correctly', () => {
356
- render(<CommandGroup variant="indented" data-testid="command-group">Content</CommandGroup>)
437
+ it('marks the group as presentation for assistive tech', () => {
438
+ renderInCommand(<CommandGroup data-testid="command-group">Content</CommandGroup>)
357
439
 
358
440
  const group = screen.getByTestId('command-group')
359
- expect(group).toHaveClass('pl-4')
441
+ expect(group).toHaveAttribute('role', 'presentation')
360
442
  })
361
443
 
362
444
  it('passes through HTML attributes', () => {
363
- render(<CommandGroup data-testid="command-group" id="group-1">Content</CommandGroup>)
445
+ renderInCommand(<CommandGroup data-testid="command-group" id="group-1">Content</CommandGroup>)
364
446
 
365
447
  const group = screen.getByTestId('command-group')
366
448
  expect(group).toHaveAttribute('id', 'group-1')
@@ -369,7 +451,7 @@ describe('Command Components', () => {
369
451
 
370
452
  describe('CommandItem Component', () => {
371
453
  it('renders correctly with default props', () => {
372
- render(<CommandItem data-testid="command-item">Item Content</CommandItem>)
454
+ renderInCommand(<CommandItem data-testid="command-item">Item Content</CommandItem>)
373
455
 
374
456
  const item = screen.getByTestId('command-item')
375
457
  expect(item).toBeInTheDocument()
@@ -378,7 +460,7 @@ describe('Command Components', () => {
378
460
  })
379
461
 
380
462
  it('applies custom className', () => {
381
- render(<CommandItem className="custom-item" data-testid="command-item">Content</CommandItem>)
463
+ renderInCommand(<CommandItem className="custom-item" data-testid="command-item">Content</CommandItem>)
382
464
 
383
465
  const item = screen.getByTestId('command-item')
384
466
  expect(item).toHaveClass('custom-item')
@@ -386,7 +468,7 @@ describe('Command Components', () => {
386
468
 
387
469
  it('forwards ref correctly', () => {
388
470
  const ref = React.createRef<HTMLDivElement>()
389
- render(<CommandItem ref={ref}>Content</CommandItem>)
471
+ renderInCommand(<CommandItem ref={ref}>Content</CommandItem>)
390
472
 
391
473
  expect(ref.current).toBeInstanceOf(HTMLDivElement)
392
474
  })
@@ -396,22 +478,28 @@ describe('Command Components', () => {
396
478
  })
397
479
 
398
480
  it('handles selected state', () => {
399
- render(<CommandItem selected={true} data-testid="command-item">Content</CommandItem>)
481
+ // Seçim, Command kökünün `value`'su ile item'ın value'sunun eşleşmesiyle olur.
482
+ renderInCommand(
483
+ <CommandItem value="test-value" data-testid="command-item">Content</CommandItem>,
484
+ { value: 'test-value' }
485
+ )
400
486
 
401
487
  const item = screen.getByTestId('command-item')
402
488
  expect(item).toHaveAttribute('aria-selected', 'true')
489
+ expect(item).toHaveAttribute('data-selected', 'true')
403
490
  })
404
491
 
405
492
  it('handles disabled state', () => {
406
- render(<CommandItem disabled={true} data-testid="command-item">Content</CommandItem>)
493
+ renderInCommand(<CommandItem disabled={true} data-testid="command-item">Content</CommandItem>)
407
494
 
408
495
  const item = screen.getByTestId('command-item')
409
- expect(item).toHaveAttribute('data-disabled', '')
496
+ expect(item).toHaveAttribute('data-disabled', 'true')
497
+ expect(item).toHaveAttribute('aria-disabled', 'true')
410
498
  })
411
499
 
412
500
  it('handles click events with onSelect', () => {
413
501
  const onSelect = jest.fn()
414
- render(
502
+ renderInCommand(
415
503
  <CommandItem onSelect={onSelect} value="test-value" data-testid="command-item">
416
504
  Content
417
505
  </CommandItem>
@@ -424,7 +512,7 @@ describe('Command Components', () => {
424
512
 
425
513
  it('does not call onSelect when disabled', () => {
426
514
  const onSelect = jest.fn()
427
- render(
515
+ renderInCommand(
428
516
  <CommandItem onSelect={onSelect} disabled={true} value="test-value" data-testid="command-item">
429
517
  Content
430
518
  </CommandItem>
@@ -435,25 +523,46 @@ describe('Command Components', () => {
435
523
  expect(onSelect).not.toHaveBeenCalled()
436
524
  })
437
525
 
438
- it('renders subtle variant correctly', () => {
439
- render(<CommandItem variant="subtle" data-testid="command-item">Content</CommandItem>)
526
+ it('filters out items that do not match the search', () => {
527
+ renderInCommand(
528
+ <>
529
+ <CommandInput data-testid="command-input" />
530
+ <CommandList>
531
+ <CommandItem value="apple">Apple</CommandItem>
532
+ <CommandItem value="banana">Banana</CommandItem>
533
+ </CommandList>
534
+ </>
535
+ )
536
+
537
+ expect(screen.getByText('Apple')).toBeInTheDocument()
538
+ expect(screen.getByText('Banana')).toBeInTheDocument()
440
539
 
441
- const item = screen.getByTestId('command-item')
442
- expect(item).toHaveClass('aria-selected:bg-accent/10')
540
+ fireEvent.change(screen.getByTestId('command-input'), { target: { value: 'ban' } })
541
+
542
+ expect(screen.queryByText('Apple')).not.toBeInTheDocument()
543
+ expect(screen.getByText('Banana')).toBeInTheDocument()
443
544
  })
444
545
 
445
- it('renders ghost variant correctly', () => {
446
- render(<CommandItem variant="ghost" data-testid="command-item">Content</CommandItem>)
546
+ it('selects the active item with the Enter key', () => {
547
+ const onSelect = jest.fn()
548
+ render(
549
+ <Command value="calendar" data-testid="command">
550
+ <CommandList>
551
+ <CommandItem value="calendar" onSelect={onSelect}>Calendar</CommandItem>
552
+ </CommandList>
553
+ </Command>
554
+ )
447
555
 
448
- const item = screen.getByTestId('command-item')
449
- expect(item).toHaveClass('hover:bg-muted/10')
556
+ fireEvent.keyDown(screen.getByTestId('command'), { key: 'Enter' })
557
+ expect(onSelect).toHaveBeenCalledWith('calendar')
450
558
  })
451
559
 
452
560
  it('passes through HTML attributes', () => {
453
- render(<CommandItem data-testid="command-item" id="item-1">Content</CommandItem>)
561
+ // NOT: `id` cmdk tarafından aria-activedescendant için üretilir ve override edilir.
562
+ renderInCommand(<CommandItem data-testid="command-item" title="Item one">Content</CommandItem>)
454
563
 
455
564
  const item = screen.getByTestId('command-item')
456
- expect(item).toHaveAttribute('id', 'item-1')
565
+ expect(item).toHaveAttribute('title', 'Item one')
457
566
  })
458
567
  })
459
568
 
@@ -487,15 +596,16 @@ describe('Command Components', () => {
487
596
 
488
597
  describe('CommandSeparator Component', () => {
489
598
  it('renders correctly with default props', () => {
490
- render(<CommandSeparator data-testid="command-separator" />)
599
+ renderInCommand(<CommandSeparator data-testid="command-separator" />)
491
600
 
492
601
  const separator = screen.getByTestId('command-separator')
493
602
  expect(separator).toBeInTheDocument()
494
603
  expect(separator).toHaveClass('-mx-1 h-px bg-border')
604
+ expect(separator).toHaveAttribute('role', 'separator')
495
605
  })
496
606
 
497
607
  it('applies custom className', () => {
498
- render(<CommandSeparator className="custom-separator" data-testid="command-separator" />)
608
+ renderInCommand(<CommandSeparator className="custom-separator" data-testid="command-separator" />)
499
609
 
500
610
  const separator = screen.getByTestId('command-separator')
501
611
  expect(separator).toHaveClass('custom-separator')
@@ -503,7 +613,7 @@ describe('Command Components', () => {
503
613
 
504
614
  it('forwards ref correctly', () => {
505
615
  const ref = React.createRef<HTMLDivElement>()
506
- render(<CommandSeparator ref={ref} />)
616
+ renderInCommand(<CommandSeparator ref={ref} />)
507
617
 
508
618
  expect(ref.current).toBeInstanceOf(HTMLDivElement)
509
619
  })
@@ -513,42 +623,61 @@ describe('Command Components', () => {
513
623
  })
514
624
 
515
625
  it('passes through HTML attributes', () => {
516
- render(<CommandSeparator data-testid="command-separator" id="separator-1" />)
626
+ renderInCommand(<CommandSeparator data-testid="command-separator" id="separator-1" />)
517
627
 
518
628
  const separator = screen.getByTestId('command-separator')
519
629
  expect(separator).toHaveAttribute('id', 'separator-1')
520
630
  })
521
631
  })
522
632
 
523
- describe('Variant Functions', () => {
524
- it('commandVariants generates correct classes', () => {
525
- expect(commandVariants()).toContain('flex h-full w-full flex-col')
526
- expect(commandVariants({ variant: 'glass' })).toContain('bg-background/80')
527
- expect(commandVariants({ size: 'lg' })).toContain('p-6')
633
+ // Kaynakta yalnızca `commandVariants` (dışa aktarılmayan) CVA haritası vardır; alt
634
+ // bileşenlerin variant prop'u yoktur. Sınıf üretimi bu yüzden public API üzerinden,
635
+ // render edilen elemanların sınıfları üzerinden doğrulanır.
636
+ describe('Variant Classes', () => {
637
+ it('Command generates correct variant and size classes', () => {
638
+ const { unmount } = render(<Command data-testid="command">Content</Command>)
639
+ expect(screen.getByTestId('command')).toHaveClass('flex h-full w-full flex-col')
640
+ expect(screen.getByTestId('command')).toHaveClass('min-h-[300px]')
641
+ unmount()
642
+
643
+ const glass = render(<Command variant="glass" data-testid="command">Content</Command>)
644
+ expect(screen.getByTestId('command')).toHaveClass('bg-background/80')
645
+ glass.unmount()
646
+
647
+ render(<Command size="lg" data-testid="command">Content</Command>)
648
+ expect(screen.getByTestId('command')).toHaveClass('min-h-[400px]')
528
649
  })
529
650
 
530
- it('commandInputVariants generates correct classes', () => {
531
- expect(commandInputVariants()).toContain('flex h-11 w-full')
532
- expect(commandInputVariants({ variant: 'minimal' })).toContain('h-9')
533
- expect(commandInputVariants({ variant: 'bordered' })).toContain('border-b')
651
+ it('CommandInput generates correct classes', () => {
652
+ renderInCommand(<CommandInput data-testid="command-input" />)
653
+
654
+ expect(screen.getByTestId('command-input')).toHaveClass(
655
+ 'flex h-11 w-full rounded-md bg-transparent py-3 text-sm'
656
+ )
534
657
  })
535
658
 
536
- it('commandListVariants generates correct classes', () => {
537
- expect(commandListVariants()).toContain('max-h-[300px]')
538
- expect(commandListVariants({ variant: 'scrollable' })).toContain('max-h-[400px]')
539
- expect(commandListVariants({ variant: 'compact' })).toContain('max-h-[200px]')
659
+ it('CommandList generates correct classes', () => {
660
+ renderInCommand(<CommandList data-testid="command-list">Content</CommandList>)
661
+
662
+ expect(screen.getByTestId('command-list')).toHaveClass(
663
+ 'max-h-[300px] overflow-y-auto overflow-x-hidden'
664
+ )
540
665
  })
541
666
 
542
- it('commandGroupVariants generates correct classes', () => {
543
- expect(commandGroupVariants()).toContain('overflow-hidden p-1')
544
- expect(commandGroupVariants({ variant: 'separated' })).toContain('border-t')
545
- expect(commandGroupVariants({ variant: 'indented' })).toContain('pl-4')
667
+ it('CommandGroup generates correct classes', () => {
668
+ renderInCommand(<CommandGroup data-testid="command-group">Content</CommandGroup>)
669
+
670
+ expect(screen.getByTestId('command-group')).toHaveClass('overflow-hidden p-1 text-foreground')
671
+ expect(screen.getByTestId('command-group')).toHaveClass('[&_[cmdk-group-heading]]:text-xs')
546
672
  })
547
673
 
548
- it('commandItemVariants generates correct classes', () => {
549
- expect(commandItemVariants()).toContain('relative flex cursor-default')
550
- expect(commandItemVariants({ variant: 'subtle' })).toContain('aria-selected:bg-accent/10')
551
- expect(commandItemVariants({ variant: 'ghost' })).toContain('hover:bg-muted/10')
674
+ it('CommandItem generates correct classes', () => {
675
+ renderInCommand(<CommandItem data-testid="command-item">Content</CommandItem>)
676
+
677
+ expect(screen.getByTestId('command-item')).toHaveClass(
678
+ 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm'
679
+ )
680
+ expect(screen.getByTestId('command-item')).toHaveClass('aria-selected:bg-accent')
552
681
  })
553
682
  })
554
683
 
@@ -561,11 +690,11 @@ describe('Command Components', () => {
561
690
  <CommandEmpty>No results found.</CommandEmpty>
562
691
  <CommandGroup heading="Suggestions">
563
692
  <CommandItem value="calendar">
564
- Calendar
693
+ <span>Calendar</span>
565
694
  <CommandShortcut>⌘C</CommandShortcut>
566
695
  </CommandItem>
567
696
  <CommandItem value="search">
568
- Search
697
+ <span>Search</span>
569
698
  <CommandShortcut>⌘S</CommandShortcut>
570
699
  </CommandItem>
571
700
  </CommandGroup>
@@ -583,6 +712,7 @@ describe('Command Components', () => {
583
712
  expect(screen.getByText('Suggestions')).toBeInTheDocument()
584
713
  expect(screen.getByText('Calendar')).toBeInTheDocument()
585
714
  expect(screen.getByText('⌘C')).toBeInTheDocument()
715
+ expect(screen.getAllByRole('option')).toHaveLength(4)
586
716
  })
587
717
 
588
718
  it('renders command dialog with all components', () => {
@@ -602,26 +732,38 @@ describe('Command Components', () => {
602
732
  expect(screen.getByTestId('dialog')).toBeInTheDocument()
603
733
  expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument()
604
734
  expect(screen.getByText('Actions')).toBeInTheDocument()
735
+ expect(screen.getByText('Action 1')).toBeInTheDocument()
605
736
  })
606
737
 
607
738
  it('handles all props and variants together', () => {
608
739
  render(
609
- <Command variant="bordered" size="lg" className="custom-command" data-testid="command">
610
- <CommandInput variant="bordered" className="custom-input" />
611
- <CommandList variant="scrollable" className="custom-list">
612
- <CommandGroup variant="separated" heading="Test Group">
613
- <CommandItem variant="subtle" selected={true} value="test">
614
- Test Item
740
+ <Command
741
+ variant="bordered"
742
+ size="lg"
743
+ value="test"
744
+ className="custom-command"
745
+ data-testid="command"
746
+ >
747
+ <CommandInput className="custom-input" data-testid="command-input" />
748
+ <CommandList className="custom-list" data-testid="command-list">
749
+ <CommandGroup heading="Test Group" data-testid="command-group">
750
+ <CommandItem value="test" data-testid="command-item">
751
+ <span>Test Item</span>
615
752
  <CommandShortcut className="custom-shortcut">⌘T</CommandShortcut>
616
753
  </CommandItem>
617
754
  </CommandGroup>
618
- <CommandSeparator className="custom-separator" />
755
+ <CommandSeparator className="custom-separator" data-testid="command-separator" />
619
756
  </CommandList>
620
757
  </Command>
621
758
  )
622
759
 
623
- const command = screen.getByTestId('command')
624
- expect(command).toHaveClass('custom-command border p-6')
760
+ expect(screen.getByTestId('command')).toHaveClass('custom-command border border-border min-h-[400px]')
761
+ expect(screen.getByTestId('command-input')).toHaveClass('custom-input')
762
+ expect(screen.getByTestId('command-list')).toHaveClass('custom-list')
763
+ expect(screen.getByTestId('command-separator')).toHaveClass('custom-separator')
764
+ expect(screen.getByText('⌘T')).toHaveClass('custom-shortcut')
765
+ // Command value'su item ile eşleştiği için item seçili render edilir
766
+ expect(screen.getByTestId('command-item')).toHaveAttribute('aria-selected', 'true')
625
767
  })
626
768
  })
627
769
 
@@ -634,16 +776,18 @@ describe('Command Components', () => {
634
776
  })
635
777
 
636
778
  it('handles command item without value', () => {
779
+ // value verilmediğinde cmdk item değerini children metninden türetir.
637
780
  const onSelect = jest.fn()
638
- render(<CommandItem onSelect={onSelect} data-testid="command-item">Item</CommandItem>)
781
+ renderInCommand(<CommandItem onSelect={onSelect} data-testid="command-item">Item</CommandItem>)
639
782
 
640
783
  const item = screen.getByTestId('command-item')
784
+ expect(item).toHaveAttribute('data-value', 'Item')
641
785
  fireEvent.click(item)
642
- expect(onSelect).not.toHaveBeenCalled()
786
+ expect(onSelect).toHaveBeenCalledWith('Item')
643
787
  })
644
788
 
645
789
  it('handles command group with complex heading', () => {
646
- render(
790
+ renderInCommand(
647
791
  <CommandGroup heading={<span className="custom-heading">Complex Heading</span>}>
648
792
  Content
649
793
  </CommandGroup>
@@ -663,13 +807,13 @@ describe('Command Components', () => {
663
807
  </Command>
664
808
  )
665
809
 
666
- expect(screen.getByRole('textbox')).toBeInTheDocument()
810
+ expect(screen.getByRole('combobox')).toBeInTheDocument()
667
811
  })
668
812
 
669
813
  it('handles disabled input', () => {
670
- render(<CommandInput disabled placeholder="Disabled input" />)
814
+ renderInCommand(<CommandInput disabled placeholder="Disabled input" />)
671
815
 
672
- const input = screen.getByRole('textbox')
816
+ const input = screen.getByRole('combobox')
673
817
  expect(input).toBeDisabled()
674
818
  expect(input).toHaveClass('disabled:cursor-not-allowed disabled:opacity-50')
675
819
  })