@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.
- package/dist/hooks/index.global.js +1 -6
- package/dist/hooks/index.global.js.map +1 -1
- package/dist/index.d.mts +33 -25
- package/dist/index.d.ts +33 -25
- package/dist/index.global.js +18 -18
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +441 -388
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +403 -353
- package/dist/index.mjs.map +1 -1
- package/dist/lib/theme.global.js +1 -14
- package/dist/lib/theme.global.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/__tests__/a11y-group-c.test.tsx +266 -0
- package/src/components/ui/__tests__/breadcrumb.test.tsx +24 -9
- package/src/components/ui/__tests__/carousel.test.tsx +37 -0
- package/src/components/ui/__tests__/quality-group-de.test.tsx +159 -0
- package/src/components/ui/breadcrumb.tsx +85 -27
- package/src/components/ui/carousel.tsx +24 -0
- package/src/components/ui/command.tsx +25 -0
- package/src/components/ui/pagination.tsx +64 -44
- package/src/components/ui/scroll-area.tsx +23 -3
- package/src/components/ui/skeleton.tsx +16 -1
- package/src/components/ui/tabs.tsx +66 -16
|
@@ -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
|
-
|
|
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
|
|
344
|
-
expect(
|
|
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
|
-
|
|
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')
|
|
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
|
-
|
|
639
|
-
expect(
|
|
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
|
+
})
|