@moontra/moonui 6.7.0 → 6.9.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.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Grup C — a11y / geçersiz işaretleme düzeltmeleri (#309 breadcrumb, #315 pagination,
3
+ * #322 tabs).
4
+ *
5
+ * Bu testlerin ortak amacı: düzeltilen kusurların HER BİRİ, düzeltme geri alındığında
6
+ * kırmızıya dönecek biçimde sabitlensin. Mevcut suite'ler bu davranışların bir kısmını
7
+ * YANLIŞ yönde sabitliyordu (ör. `aria-current`'ı <li>'de, `aria-hidden`'ı ellipsis
8
+ * sarmalayıcısında doğruluyorlardı) — onlar da bu PR'da gerçeğe çekildi.
9
+ */
10
+ import React from 'react'
11
+ import { render, screen } from '@testing-library/react'
12
+ import '@testing-library/jest-dom'
13
+
14
+ import {
15
+ Breadcrumb,
16
+ BreadcrumbList,
17
+ BreadcrumbItem,
18
+ BreadcrumbEllipsis,
19
+ BreadcrumbSeparator,
20
+ } from '../breadcrumb'
21
+ import {
22
+ Pagination,
23
+ PaginationContent,
24
+ PaginationItem,
25
+ PaginationLink,
26
+ PaginationPrevious,
27
+ PaginationNext,
28
+ PaginationEllipsis,
29
+ } from '../pagination'
30
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from '../tabs'
31
+
32
+ /**
33
+ * Bir elemanın erişilebilirlik ağacından düşüp düşmediğini ata zincirine bakarak belirler.
34
+ * `getByText` aria-hidden'ı UMURSAMAZ — bu yüzden "metin DOM'da mı" testi, metnin ekran
35
+ * okuyucuya ULAŞTIĞINI kanıtlamaz. Grup C'nin iki kusuru da tam bu boşlukta saklanmıştı.
36
+ */
37
+ function isHiddenFromAssistiveTech(el: Element | null): boolean {
38
+ let node: Element | null = el
39
+ while (node) {
40
+ if (node.getAttribute('aria-hidden') === 'true') return true
41
+ node = node.parentElement
42
+ }
43
+ return false
44
+ }
45
+
46
+ describe('#309 breadcrumb', () => {
47
+ it('asChild: className KAYBOLMAZ ve React geçersiz-prop uyarısı basılmaz', () => {
48
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
49
+
50
+ render(
51
+ <BreadcrumbItem asChild>
52
+ <a href="/docs" data-testid="child-link">
53
+ Docs
54
+ </a>
55
+ </BreadcrumbItem>
56
+ )
57
+
58
+ const link = screen.getByTestId('child-link')
59
+ // Radix Slot, kendi className'ini çocuğunkiyle BİRLEŞTİRİR. React.Fragment yolunda
60
+ // className tümüyle düşüyordu.
61
+ expect(link.className).toContain('transition-colors')
62
+ expect(link).toHaveAttribute('href', '/docs')
63
+ expect(errorSpy).not.toHaveBeenCalled()
64
+
65
+ errorSpy.mockRestore()
66
+ })
67
+
68
+ it('asChild + isCurrent: aria-current çocuğa iner', () => {
69
+ render(
70
+ <BreadcrumbItem asChild isCurrent>
71
+ <span data-testid="child-page">Current</span>
72
+ </BreadcrumbItem>
73
+ )
74
+ expect(screen.getByTestId('child-page')).toHaveAttribute('aria-current', 'page')
75
+ })
76
+
77
+ it('ellipsis: "More pages" metni erişilebilirlik ağacında', () => {
78
+ render(<BreadcrumbEllipsis />)
79
+ const srText = screen.getByText('More pages')
80
+ expect(isHiddenFromAssistiveTech(srText)).toBe(false)
81
+ })
82
+
83
+ it('ellipsis: dekoratif ikon gizli KALIR', () => {
84
+ const { container } = render(<BreadcrumbEllipsis />)
85
+ const svg = container.querySelector('svg')
86
+ expect(svg).not.toBeNull()
87
+ expect(isHiddenFromAssistiveTech(svg)).toBe(true)
88
+ })
89
+
90
+ it('separator/ellipsis: `icon` prop\'u okunur, DOM\'a sızmaz', () => {
91
+ const { container: sep } = render(
92
+ <BreadcrumbSeparator icon={<i data-testid="sep-icon" />} />
93
+ )
94
+ expect(sep.querySelector('[data-testid="sep-icon"]')).not.toBeNull()
95
+ expect(sep.querySelector('[icon]')).toBeNull()
96
+
97
+ const { container: ell } = render(
98
+ <BreadcrumbEllipsis icon={<i data-testid="ell-icon" />} />
99
+ )
100
+ expect(ell.querySelector('[data-testid="ell-icon"]')).not.toBeNull()
101
+ expect(ell.querySelector('[icon]')).toBeNull()
102
+ })
103
+
104
+ it('kök `separator` alt ayraçlara iner; yerel icon/children onu EZER', () => {
105
+ const { container } = render(
106
+ <Breadcrumb separator={<i data-testid="root-sep" />}>
107
+ <BreadcrumbList>
108
+ <BreadcrumbItem>Home</BreadcrumbItem>
109
+ <BreadcrumbSeparator />
110
+ <BreadcrumbItem>Docs</BreadcrumbItem>
111
+ <BreadcrumbSeparator icon={<i data-testid="local-sep" />} />
112
+ <BreadcrumbItem isCurrent>Breadcrumb</BreadcrumbItem>
113
+ </BreadcrumbList>
114
+ </Breadcrumb>
115
+ )
116
+
117
+ // Kök ayraç context ile indi
118
+ expect(container.querySelectorAll('[data-testid="root-sep"]')).toHaveLength(1)
119
+ // Yerel `icon` kökü ezdi
120
+ expect(container.querySelectorAll('[data-testid="local-sep"]')).toHaveLength(1)
121
+ })
122
+
123
+ it('separator/showHomeIcon `<nav>` DOM\'una SIZMAZ', () => {
124
+ const { container } = render(
125
+ <Breadcrumb separator={<i />} showHomeIcon>
126
+ <BreadcrumbList>
127
+ <BreadcrumbItem>Home</BreadcrumbItem>
128
+ </BreadcrumbList>
129
+ </Breadcrumb>
130
+ )
131
+
132
+ const nav = container.querySelector('nav')!
133
+ // Eskiden ikisi de destructure edilmiyor, `{...props}` ile öznitelik olarak basılıyordu.
134
+ expect(nav.hasAttribute('separator')).toBe(false)
135
+ expect(nav.hasAttribute('showhomeicon')).toBe(false)
136
+ })
137
+ })
138
+
139
+ describe('#315 pagination', () => {
140
+ it('ellipsis: "More pages" metni erişilebilirlik ağacında', () => {
141
+ render(<PaginationEllipsis />)
142
+ const srText = screen.getByText('More pages')
143
+ expect(isHiddenFromAssistiveTech(srText)).toBe(false)
144
+ })
145
+
146
+ it('Previous/Next etiketi küçük ekranda gizlenir, erişilebilir ad korunur', () => {
147
+ render(
148
+ <Pagination>
149
+ <PaginationContent>
150
+ <PaginationItem>
151
+ <PaginationPrevious href="#" />
152
+ </PaginationItem>
153
+ <PaginationItem>
154
+ <PaginationNext href="#" />
155
+ </PaginationItem>
156
+ </PaginationContent>
157
+ </Pagination>
158
+ )
159
+
160
+ // Görsel etiket `sm` altında display:none — 375px'te 88px'lik taşmanın kaynağıydı.
161
+ expect(screen.getByText('Previous')).toHaveClass('hidden', 'sm:inline')
162
+ expect(screen.getByText('Next')).toHaveClass('hidden', 'sm:inline')
163
+
164
+ // Ad `aria-label`'dan geldiği için ekran-okuyucu deneyimi değişmez.
165
+ expect(screen.getByLabelText('Go to previous page')).toBeInTheDocument()
166
+ expect(screen.getByLabelText('Go to next page')).toBeInTheDocument()
167
+ })
168
+
169
+ it('beş alt-component ref iletir', () => {
170
+ const refs = {
171
+ nav: React.createRef<HTMLElement>(),
172
+ link: React.createRef<HTMLAnchorElement>(),
173
+ prev: React.createRef<HTMLAnchorElement>(),
174
+ next: React.createRef<HTMLAnchorElement>(),
175
+ ellipsis: React.createRef<HTMLSpanElement>(),
176
+ }
177
+
178
+ render(
179
+ <Pagination ref={refs.nav}>
180
+ <PaginationContent>
181
+ <PaginationItem>
182
+ <PaginationLink ref={refs.link} href="#">
183
+ 1
184
+ </PaginationLink>
185
+ </PaginationItem>
186
+ <PaginationItem>
187
+ <PaginationPrevious ref={refs.prev} href="#" />
188
+ </PaginationItem>
189
+ <PaginationItem>
190
+ <PaginationNext ref={refs.next} href="#" />
191
+ </PaginationItem>
192
+ <PaginationItem>
193
+ <PaginationEllipsis ref={refs.ellipsis} />
194
+ </PaginationItem>
195
+ </PaginationContent>
196
+ </Pagination>
197
+ )
198
+
199
+ expect(refs.nav.current?.tagName).toBe('NAV')
200
+ expect(refs.link.current?.tagName).toBe('A')
201
+ expect(refs.prev.current?.tagName).toBe('A')
202
+ expect(refs.next.current?.tagName).toBe('A')
203
+ expect(refs.ellipsis.current?.tagName).toBe('SPAN')
204
+ })
205
+ })
206
+
207
+ describe('#322 tabs (free)', () => {
208
+ const renderTabs = (props: React.ComponentProps<typeof Tabs> = {}) =>
209
+ render(
210
+ <Tabs defaultValue="a" {...props}>
211
+ <TabsList data-testid="list">
212
+ <TabsTrigger value="a" data-testid="trigger">
213
+ A
214
+ </TabsTrigger>
215
+ </TabsList>
216
+ <TabsContent value="a">içerik</TabsContent>
217
+ </Tabs>
218
+ )
219
+
220
+ it('orientation="vertical" Root + List + Trigger\'ın ÜÇÜNE birden iner', () => {
221
+ const { container } = renderTabs({ orientation: 'vertical' })
222
+
223
+ // Root: Radix'in kendi data-özniteliği
224
+ expect(container.querySelector('[data-orientation="vertical"]')).not.toBeNull()
225
+ // List ve Trigger: CVA dikey sınıfları — eskiden bunlar Root'tan HİÇ beslenmiyordu,
226
+ // sonuç: klavye yönü dikey, görünüm yatay.
227
+ expect(screen.getByTestId('list').className).toContain('flex-col')
228
+ expect(screen.getByTestId('trigger').className).toContain('justify-start')
229
+ })
230
+
231
+ it('vertical={true} (deprecated) hâlâ çalışır', () => {
232
+ renderTabs({ vertical: true })
233
+ expect(screen.getByTestId('list').className).toContain('flex-col')
234
+ })
235
+
236
+ it('açık orientation, vertical prop\'unu EZER', () => {
237
+ renderTabs({ vertical: true, orientation: 'horizontal' })
238
+ expect(screen.getByTestId('list').className).toContain('flex-row')
239
+ expect(screen.getByTestId('list').className).not.toContain('flex-col')
240
+ })
241
+
242
+ it('alt-component kendi orientation\'ını verirse context\'i ezer', () => {
243
+ render(
244
+ <Tabs defaultValue="a" orientation="vertical">
245
+ <TabsList data-testid="list" orientation="horizontal">
246
+ <TabsTrigger value="a">A</TabsTrigger>
247
+ </TabsList>
248
+ </Tabs>
249
+ )
250
+ expect(screen.getByTestId('list').className).toContain('flex-row')
251
+ })
252
+
253
+ it('Root className verildiğinde `moonui-theme` KAYBOLMAZ', () => {
254
+ const { container } = render(
255
+ <Tabs defaultValue="a" className="custom-root" data-testid="root">
256
+ <TabsList>
257
+ <TabsTrigger value="a">A</TabsTrigger>
258
+ </TabsList>
259
+ </Tabs>
260
+ )
261
+ const root = container.querySelector('[data-testid="root"]')!
262
+ // Eskiden `{...props}` cn() sonucunu tümüyle eziyordu → tema kapsam sınıfı düşüyordu.
263
+ expect(root.className).toContain('moonui-theme')
264
+ expect(root.className).toContain('custom-root')
265
+ })
266
+ })
@@ -333,15 +333,24 @@ describe('Breadcrumb Components', () => {
333
333
  expect(button).toHaveTextContent('Custom Button')
334
334
  })
335
335
 
336
- it('sets aria-current when isCurrent is true', () => {
336
+ // #309: `aria-current` sarmalayıcı <li>'den, geçerli sayfayı TEMSİL EDEN elemana taşındı
337
+ // (APG breadcrumb deseni). Bu test eskiden <li>'yi doğruluyordu — yani daha az doğru olan
338
+ // yerleşimi sabitliyordu.
339
+ it('sets aria-current on the page element (not the wrapper li)', () => {
337
340
  render(
338
341
  <BreadcrumbItem isCurrent data-testid="breadcrumb-item">
339
342
  Current Page
340
343
  </BreadcrumbItem>
341
344
  )
342
-
343
- const item = screen.getByTestId('breadcrumb-item')
344
- expect(item).toHaveAttribute('aria-current', 'page')
345
+
346
+ const wrapper = screen.getByTestId('breadcrumb-item')
347
+ expect(wrapper.tagName).toBe('LI')
348
+ expect(wrapper).not.toHaveAttribute('aria-current')
349
+
350
+ const pageEl = screen.getByText('Current Page')
351
+ expect(pageEl).toHaveAttribute('aria-current', 'page')
352
+ // Çift duyuruyu önlemek için tam olarak BİR eleman taşımalı
353
+ expect(document.querySelectorAll('[aria-current="page"]')).toHaveLength(1)
345
354
  })
346
355
 
347
356
  it('does not set aria-current when isCurrent is false', () => {
@@ -467,7 +476,11 @@ describe('Breadcrumb Components', () => {
467
476
  // Ellipsis de <span> olarak render ediliyor.
468
477
  expect(ellipsis.tagName).toBe('SPAN')
469
478
  expect(ellipsis).toHaveAttribute('role', 'presentation')
470
- expect(ellipsis).toHaveAttribute('aria-hidden', 'true')
479
+ // #309: bu satır eskiden `aria-hidden="true"` DOĞRULUYORDU — yani component'in
480
+ // "More pages" metnini ekran okuyucudan gizleyen kusurunu sabitliyordu. Gizleme
481
+ // artık yalnız dekoratif ikonun sarmalayıcısında.
482
+ expect(ellipsis).not.toHaveAttribute('aria-hidden')
483
+ expect(ellipsis.querySelector('[aria-hidden="true"]')).not.toBeNull()
471
484
  })
472
485
 
473
486
  it('applies custom className', () => {
@@ -539,11 +552,13 @@ describe('Breadcrumb Components', () => {
539
552
 
540
553
  const homeLink = screen.getByText('Home').closest('a')
541
554
  const productsLink = screen.getByText('Products').closest('a')
542
- const currentItem = screen.getByText('Current Product').closest('li')
543
-
555
+ const currentItem = screen.getByText('Current Product')
556
+
544
557
  expect(homeLink).toHaveAttribute('href', '/')
545
558
  expect(productsLink).toHaveAttribute('href', '/products')
559
+ // #309: <li>'de değil, geçerli sayfa elemanında
546
560
  expect(currentItem).toHaveAttribute('aria-current', 'page')
561
+ expect(currentItem.closest('li')).not.toHaveAttribute('aria-current')
547
562
  })
548
563
 
549
564
  it('renders with custom separators', () => {
@@ -635,8 +650,8 @@ describe('Breadcrumb Components', () => {
635
650
  )
636
651
 
637
652
  expect(screen.getByText('Only Item')).toBeInTheDocument()
638
- const item = screen.getByText('Only Item').closest('li')
639
- expect(item).toHaveAttribute('aria-current', 'page')
653
+ // #309: <li>'de değil, geçerli sayfa elemanında
654
+ expect(screen.getByText('Only Item')).toHaveAttribute('aria-current', 'page')
640
655
  })
641
656
 
642
657
  it('handles breadcrumb with only separators', () => {
@@ -10,6 +10,11 @@ jest.mock('embla-carousel-react', () => {
10
10
  canScrollNext: jest.fn(() => true),
11
11
  scrollPrev: jest.fn(),
12
12
  scrollNext: jest.fn(),
13
+ // #308-2: slayt duyurusu ("Slide 2 of 5") bu iki metoda dayanır. Mock gerçek embla
14
+ // API'sinin alt kümesidir; eksik kalırsa component'in kusuru değil FIXTURE'ın eksiği
15
+ // olarak patlar (ilk yazımda 23 test bu yüzden kırmızıya döndü).
16
+ selectedScrollSnap: jest.fn(() => 0),
17
+ scrollSnapList: jest.fn(() => [0, 0.5, 1]),
13
18
  on: jest.fn(),
14
19
  off: jest.fn(),
15
20
  }
@@ -36,6 +41,8 @@ const embla = jest.requireMock('embla-carousel-react') as {
36
41
  canScrollNext: jest.Mock
37
42
  scrollPrev: jest.Mock
38
43
  scrollNext: jest.Mock
44
+ selectedScrollSnap: jest.Mock
45
+ scrollSnapList: jest.Mock
39
46
  on: jest.Mock
40
47
  off: jest.Mock
41
48
  }
@@ -59,6 +66,8 @@ describe('Carousel Components', () => {
59
66
  jest.clearAllMocks()
60
67
  mockApi.canScrollPrev.mockReturnValue(true)
61
68
  mockApi.canScrollNext.mockReturnValue(true)
69
+ mockApi.selectedScrollSnap.mockReturnValue(0)
70
+ mockApi.scrollSnapList.mockReturnValue([0, 0.5, 1])
62
71
  })
63
72
 
64
73
  describe('Carousel Root', () => {
@@ -71,6 +80,34 @@ describe('Carousel Components', () => {
71
80
  expect(carousel).toHaveAttribute('aria-roledescription', 'carousel')
72
81
  })
73
82
 
83
+ /**
84
+ * #308-2: `aria-roledescription` doğru set edilmişti ama slayt DEĞİŞİMİNİ duyuran
85
+ * hiçbir şey yoktu — ekran-okuyucu kullanıcısı ilerledikten sonra hangi slaytta
86
+ * olduğunu öğrenemiyordu. Biçim burada, embla mock'landığı için deterministik
87
+ * doğrulanabiliyor (gerçek embla jsdom'da ölçüm yapamaz).
88
+ */
89
+ it('slayt değişimini aria-live bölgesinde duyurur', () => {
90
+ mockApi.selectedScrollSnap.mockReturnValue(1)
91
+ mockApi.scrollSnapList.mockReturnValue([0, 0.5, 1])
92
+
93
+ const { container } = renderCarousel()
94
+ const live = container.querySelector('[aria-live="polite"]')
95
+
96
+ expect(live).not.toBeNull()
97
+ expect(live).toHaveAttribute('aria-atomic', 'true')
98
+ expect(live).toHaveClass('sr-only')
99
+ // selectedScrollSnap 0-tabanlı → kullanıcıya 1-tabanlı gösterilir
100
+ expect(live).toHaveTextContent('Slide 2 of 3')
101
+ })
102
+
103
+ it('slayt sayısı bilinmiyorken BOŞ duyuru basar', () => {
104
+ mockApi.scrollSnapList.mockReturnValue([])
105
+
106
+ const { container } = renderCarousel()
107
+ // Aksi hâlde ekran okuyucuya "Slide 1 of 0" giderdi.
108
+ expect(container.querySelector('[aria-live="polite"]')!.textContent).toBe('')
109
+ })
110
+
74
111
  it('applies moonui-theme and custom className', () => {
75
112
  renderCarousel({ className: 'custom-carousel' })
76
113
 
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Grup D+E — #320 skeleton, #308 bundle (free tarafı).
3
+ *
4
+ * Her test, düzeltme geri alındığında kırmızıya dönecek biçimde yazıldı; canary ile
5
+ * doğrulandı (bkz. changelog).
6
+ */
7
+ import React from 'react'
8
+ import { render, screen } from '@testing-library/react'
9
+ import '@testing-library/jest-dom'
10
+
11
+ import { Skeleton, SkeletonText } from '../skeleton'
12
+ import { ScrollArea } from '../scroll-area'
13
+ import {
14
+ Command,
15
+ CommandInput,
16
+ CommandList,
17
+ CommandGroup,
18
+ CommandItem,
19
+ CommandSeparator,
20
+ } from '../command'
21
+ import {
22
+ Carousel,
23
+ CarouselContent,
24
+ CarouselItem,
25
+ } from '../carousel'
26
+
27
+ // cmdk seçili öğeyi görünür alana kaydırır; jsdom `scrollIntoView` implement etmez.
28
+ // Ortam eksiği — test edilen davranışla ilgisi yok.
29
+ beforeAll(() => {
30
+ if (!Element.prototype.scrollIntoView) {
31
+ Element.prototype.scrollIntoView = function scrollIntoView() {}
32
+ }
33
+ })
34
+
35
+ describe('#320 skeleton — a11y konvansiyonu', () => {
36
+ it('yer tutucu erişilebilirlik ağacından çıkarılır', () => {
37
+ const { container } = render(<Skeleton data-testid="sk" />)
38
+ expect(container.querySelector('[data-testid="sk"]')).toHaveAttribute('aria-hidden', 'true')
39
+ })
40
+
41
+ it('N skeleton, N canlı bölge ÜRETMEZ (role="status" seçilmemesinin gerekçesi)', () => {
42
+ const { container } = render(<SkeletonText lines={5} />)
43
+ // Tek yükleme durumu onlarca skeleton basar; her biri canlı bölge olsaydı ekran
44
+ // okuyucu aynı olayı N kez duyururdu. Duyurunun sahibi spinner'dır.
45
+ expect(container.querySelectorAll('[role="status"]')).toHaveLength(0)
46
+ expect(container.querySelectorAll('[aria-live]')).toHaveLength(0)
47
+ })
48
+
49
+ it('isLoaded && children dalı GERÇEK içerik → gizlenmez', () => {
50
+ render(
51
+ <Skeleton isLoaded data-testid="loaded">
52
+ <span>gerçek içerik</span>
53
+ </Skeleton>
54
+ )
55
+ const el = screen.getByTestId('loaded')
56
+ expect(el).not.toHaveAttribute('aria-hidden')
57
+ expect(screen.getByText('gerçek içerik')).toBeInTheDocument()
58
+ })
59
+ })
60
+
61
+ describe('#308-1 scroll-area — klavye erişimi', () => {
62
+ it('viewport varsayılan olarak odaklanabilir (WCAG 2.1.1)', () => {
63
+ const { container } = render(
64
+ <ScrollArea className="h-20">
65
+ <p>odaklanabilir eleman içermeyen içerik</p>
66
+ </ScrollArea>
67
+ )
68
+ const viewport = container.querySelector('[data-radix-scroll-area-viewport]')
69
+ expect(viewport).not.toBeNull()
70
+ expect(viewport).toHaveAttribute('tabindex', '0')
71
+ })
72
+
73
+ it('focusable={false} ile sekme durağı kaldırılabilir', () => {
74
+ const { container } = render(
75
+ <ScrollArea focusable={false} className="h-20">
76
+ <p>içerik</p>
77
+ </ScrollArea>
78
+ )
79
+ expect(container.querySelector('[data-radix-scroll-area-viewport]'))
80
+ .not.toHaveAttribute('tabindex')
81
+ })
82
+ })
83
+
84
+ describe('#308-2 carousel — slayt duyurusu', () => {
85
+ it('aria-live durum bölgesi var ve atomik', () => {
86
+ const { container } = render(
87
+ <Carousel>
88
+ <CarouselContent>
89
+ <CarouselItem>1</CarouselItem>
90
+ <CarouselItem>2</CarouselItem>
91
+ </CarouselContent>
92
+ </Carousel>
93
+ )
94
+ const live = container.querySelector('[aria-live="polite"]')
95
+ expect(live).not.toBeNull()
96
+ expect(live).toHaveAttribute('aria-atomic', 'true')
97
+ expect(live).toHaveClass('sr-only')
98
+ })
99
+
100
+ /**
101
+ * Duyuru METNİNİN biçimi (`"Slide 2 of 5"`) burada DOĞRULANMIYOR — kasıtlı.
102
+ * jsdom'da her eleman sıfır boyutlu olduğu için embla'nın `scrollSnapList()` sonucu
103
+ * güvenilmez: tek item'lı carousel'de 1 snap, üç item'lıda 0 snap raporlandı. Bu sayıya
104
+ * dayanan bir assert, ürünü değil jsdom'un düzen eksikliğini sabitlerdi.
105
+ * Biçim ve slayt değişiminde güncellenme GERÇEK TARAYICIDA doğrulandı (bkz. changelog).
106
+ */
107
+ it('slayt sayısı BİLİNMİYORKEN boş duyuru basmaz', () => {
108
+ // Embla init olmadan (`total === 0`) metin boş kalmalı — aksi hâlde ekran okuyucuya
109
+ // "Slide 0 of 0" gider. Slayt İÇERMEYEN carousel bu durumu doğrudan üretir.
110
+ const { container } = render(
111
+ <Carousel>
112
+ <CarouselContent />
113
+ </Carousel>
114
+ )
115
+ expect(container.querySelector('[aria-live="polite"]')!.textContent).toBe('')
116
+ })
117
+ })
118
+
119
+ describe('#308-5/6 command', () => {
120
+ it('separator erişilebilirlik ağacından çıkarılır (listbox çocuk kuralı)', () => {
121
+ const { container } = render(
122
+ <Command>
123
+ <CommandInput placeholder="ara" />
124
+ <CommandList>
125
+ <CommandGroup heading="A">
126
+ <CommandItem value="bir">Bir</CommandItem>
127
+ </CommandGroup>
128
+ <CommandSeparator data-testid="sep" alwaysRender />
129
+ <CommandGroup heading="B">
130
+ <CommandItem value="iki">İki</CommandItem>
131
+ </CommandGroup>
132
+ </CommandList>
133
+ </Command>
134
+ )
135
+
136
+ const sep = container.querySelector('[data-testid="sep"]')
137
+ expect(sep).not.toBeNull()
138
+ // cmdk `role="separator"` basar ve prop'lardan SONRA yazdığı için ezilemez;
139
+ // `aria-hidden` ile ağaçtan çıkarılır → listbox'ın sahiplendiği çocuk olmaktan çıkar.
140
+ expect(sep).toHaveAttribute('aria-hidden', 'true')
141
+ })
142
+
143
+ it('listbox\'ın gizlenmemiş çocuğu separator DEĞİL', () => {
144
+ const { container } = render(
145
+ <Command>
146
+ <CommandList>
147
+ <CommandGroup heading="A">
148
+ <CommandItem value="bir">Bir</CommandItem>
149
+ </CommandGroup>
150
+ <CommandSeparator alwaysRender />
151
+ </CommandList>
152
+ </Command>
153
+ )
154
+ const listbox = container.querySelector('[role="listbox"]')!
155
+ const gorunurCocuklar = Array.from(listbox.querySelectorAll('[role="separator"]'))
156
+ .filter((el) => el.getAttribute('aria-hidden') !== 'true')
157
+ expect(gorunurCocuklar).toHaveLength(0)
158
+ })
159
+ })