@moontra/moonui 3.0.1 → 3.2.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/chunk-X5ULQZNC.global.js +33 -0
- package/dist/chunk-X5ULQZNC.global.js.map +1 -0
- package/dist/index.d.mts +169 -2
- package/dist/index.d.ts +169 -2
- package/dist/index.global.js +481 -151
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +989 -288
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +903 -253
- package/dist/index.mjs.map +1 -1
- package/dist/lib/theme.d.mts +1 -1
- package/dist/lib/theme.d.ts +1 -1
- package/dist/lib/theme.global.js +6 -30
- package/dist/lib/theme.global.js.map +1 -1
- package/dist/lib/theme.js +11 -6
- package/dist/lib/theme.js.map +1 -1
- package/dist/lib/theme.mjs +11 -6
- package/dist/lib/theme.mjs.map +1 -1
- package/package.json +3 -1
- package/src/components/ui/__tests__/carousel.test.tsx +291 -0
- package/src/components/ui/__tests__/input-otp.test.tsx +228 -0
- package/src/components/ui/__tests__/kbd.test.tsx +86 -0
- package/src/components/ui/__tests__/rating.test.tsx +218 -0
- package/src/components/ui/__tests__/spinner.test.tsx +141 -0
- package/src/components/ui/carousel.tsx +324 -0
- package/src/components/ui/index.ts +56 -0
- package/src/components/ui/input-otp.tsx +140 -0
- package/src/components/ui/kbd.tsx +66 -0
- package/src/components/ui/rating.tsx +253 -0
- package/src/components/ui/spinner.tsx +112 -0
- package/tailwind-preset.js +6 -0
- package/dist/chunk-PLK4O6WY.global.js +0 -33
- package/dist/chunk-PLK4O6WY.global.js.map +0 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import userEvent from '@testing-library/user-event'
|
|
4
|
+
|
|
5
|
+
// Embla, jsdom'da boyut ölçemediği için (tüm elemanlar 0px) gerçek scroll davranışı
|
|
6
|
+
// üretemez — API'yi deterministik test edebilmek için embla-carousel-react mock'lanır.
|
|
7
|
+
jest.mock('embla-carousel-react', () => {
|
|
8
|
+
const mockApi = {
|
|
9
|
+
canScrollPrev: jest.fn(() => true),
|
|
10
|
+
canScrollNext: jest.fn(() => true),
|
|
11
|
+
scrollPrev: jest.fn(),
|
|
12
|
+
scrollNext: jest.fn(),
|
|
13
|
+
on: jest.fn(),
|
|
14
|
+
off: jest.fn(),
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
__esModule: true,
|
|
18
|
+
default: jest.fn(() => [jest.fn(), mockApi]),
|
|
19
|
+
__mockApi: mockApi,
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
Carousel,
|
|
25
|
+
CarouselContent,
|
|
26
|
+
CarouselItem,
|
|
27
|
+
CarouselPrevious,
|
|
28
|
+
CarouselNext,
|
|
29
|
+
useCarousel,
|
|
30
|
+
} from '../carousel'
|
|
31
|
+
|
|
32
|
+
const embla = jest.requireMock('embla-carousel-react') as {
|
|
33
|
+
default: jest.Mock
|
|
34
|
+
__mockApi: {
|
|
35
|
+
canScrollPrev: jest.Mock
|
|
36
|
+
canScrollNext: jest.Mock
|
|
37
|
+
scrollPrev: jest.Mock
|
|
38
|
+
scrollNext: jest.Mock
|
|
39
|
+
on: jest.Mock
|
|
40
|
+
off: jest.Mock
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const mockApi = embla.__mockApi
|
|
44
|
+
|
|
45
|
+
const renderCarousel = (props: React.ComponentProps<typeof Carousel> = {}) =>
|
|
46
|
+
render(
|
|
47
|
+
<Carousel data-testid="carousel" {...props}>
|
|
48
|
+
<CarouselContent data-testid="carousel-content">
|
|
49
|
+
<CarouselItem data-testid="carousel-item-1">Slide 1</CarouselItem>
|
|
50
|
+
<CarouselItem data-testid="carousel-item-2">Slide 2</CarouselItem>
|
|
51
|
+
</CarouselContent>
|
|
52
|
+
<CarouselPrevious data-testid="carousel-previous" />
|
|
53
|
+
<CarouselNext data-testid="carousel-next" />
|
|
54
|
+
</Carousel>
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
describe('Carousel Components', () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
jest.clearAllMocks()
|
|
60
|
+
mockApi.canScrollPrev.mockReturnValue(true)
|
|
61
|
+
mockApi.canScrollNext.mockReturnValue(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('Carousel Root', () => {
|
|
65
|
+
it('renders with carousel accessibility attributes', () => {
|
|
66
|
+
renderCarousel()
|
|
67
|
+
|
|
68
|
+
const carousel = screen.getByTestId('carousel')
|
|
69
|
+
expect(carousel).toBeInTheDocument()
|
|
70
|
+
expect(carousel).toHaveAttribute('role', 'region')
|
|
71
|
+
expect(carousel).toHaveAttribute('aria-roledescription', 'carousel')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('applies moonui-theme and custom className', () => {
|
|
75
|
+
renderCarousel({ className: 'custom-carousel' })
|
|
76
|
+
|
|
77
|
+
const carousel = screen.getByTestId('carousel')
|
|
78
|
+
expect(carousel).toHaveClass('moonui-theme', 'relative', 'custom-carousel')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('forwards ref correctly', () => {
|
|
82
|
+
const ref = React.createRef<HTMLDivElement>()
|
|
83
|
+
render(
|
|
84
|
+
<Carousel ref={ref}>
|
|
85
|
+
<CarouselContent>
|
|
86
|
+
<CarouselItem>Slide 1</CarouselItem>
|
|
87
|
+
</CarouselContent>
|
|
88
|
+
</Carousel>
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('maintains displayName', () => {
|
|
95
|
+
expect(Carousel.displayName).toBe('Carousel')
|
|
96
|
+
expect(CarouselContent.displayName).toBe('CarouselContent')
|
|
97
|
+
expect(CarouselItem.displayName).toBe('CarouselItem')
|
|
98
|
+
expect(CarouselPrevious.displayName).toBe('CarouselPrevious')
|
|
99
|
+
expect(CarouselNext.displayName).toBe('CarouselNext')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('calls setApi with the embla api', () => {
|
|
103
|
+
const setApi = jest.fn()
|
|
104
|
+
renderCarousel({ setApi })
|
|
105
|
+
|
|
106
|
+
expect(setApi).toHaveBeenCalledWith(mockApi)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('passes horizontal axis to embla by default', () => {
|
|
110
|
+
renderCarousel()
|
|
111
|
+
|
|
112
|
+
expect(embla.default).toHaveBeenCalledWith(
|
|
113
|
+
expect.objectContaining({ axis: 'x' }),
|
|
114
|
+
undefined
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('passes vertical axis to embla when orientation is vertical', () => {
|
|
119
|
+
renderCarousel({ orientation: 'vertical' })
|
|
120
|
+
|
|
121
|
+
expect(embla.default).toHaveBeenCalledWith(
|
|
122
|
+
expect.objectContaining({ axis: 'y' }),
|
|
123
|
+
undefined
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('forwards opts and plugins to embla', () => {
|
|
128
|
+
const plugins: never[] = []
|
|
129
|
+
renderCarousel({ opts: { loop: true }, plugins })
|
|
130
|
+
|
|
131
|
+
expect(embla.default).toHaveBeenCalledWith(
|
|
132
|
+
expect.objectContaining({ loop: true, axis: 'x' }),
|
|
133
|
+
plugins
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('subscribes to embla select and reInit events', () => {
|
|
138
|
+
renderCarousel()
|
|
139
|
+
|
|
140
|
+
expect(mockApi.on).toHaveBeenCalledWith('select', expect.any(Function))
|
|
141
|
+
expect(mockApi.on).toHaveBeenCalledWith('reInit', expect.any(Function))
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('Keyboard Navigation', () => {
|
|
146
|
+
it('scrolls next on ArrowRight in horizontal orientation', () => {
|
|
147
|
+
renderCarousel()
|
|
148
|
+
|
|
149
|
+
fireEvent.keyDown(screen.getByTestId('carousel'), { key: 'ArrowRight' })
|
|
150
|
+
expect(mockApi.scrollNext).toHaveBeenCalledTimes(1)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('scrolls prev on ArrowLeft in horizontal orientation', () => {
|
|
154
|
+
renderCarousel()
|
|
155
|
+
|
|
156
|
+
fireEvent.keyDown(screen.getByTestId('carousel'), { key: 'ArrowLeft' })
|
|
157
|
+
expect(mockApi.scrollPrev).toHaveBeenCalledTimes(1)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('scrolls with ArrowUp/ArrowDown in vertical orientation', () => {
|
|
161
|
+
renderCarousel({ orientation: 'vertical' })
|
|
162
|
+
|
|
163
|
+
const carousel = screen.getByTestId('carousel')
|
|
164
|
+
fireEvent.keyDown(carousel, { key: 'ArrowDown' })
|
|
165
|
+
expect(mockApi.scrollNext).toHaveBeenCalledTimes(1)
|
|
166
|
+
|
|
167
|
+
fireEvent.keyDown(carousel, { key: 'ArrowUp' })
|
|
168
|
+
expect(mockApi.scrollPrev).toHaveBeenCalledTimes(1)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('ignores horizontal arrow keys in vertical orientation', () => {
|
|
172
|
+
renderCarousel({ orientation: 'vertical' })
|
|
173
|
+
|
|
174
|
+
fireEvent.keyDown(screen.getByTestId('carousel'), { key: 'ArrowRight' })
|
|
175
|
+
expect(mockApi.scrollNext).not.toHaveBeenCalled()
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('CarouselContent', () => {
|
|
180
|
+
it('applies horizontal orientation classes by default', () => {
|
|
181
|
+
renderCarousel()
|
|
182
|
+
|
|
183
|
+
const content = screen.getByTestId('carousel-content')
|
|
184
|
+
expect(content).toHaveClass('flex', '-ml-4')
|
|
185
|
+
// Dış sarmalayıcı overflow-hidden olmalı (embla viewport)
|
|
186
|
+
expect(content.parentElement).toHaveClass('overflow-hidden')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('applies vertical orientation classes', () => {
|
|
190
|
+
renderCarousel({ orientation: 'vertical' })
|
|
191
|
+
|
|
192
|
+
const content = screen.getByTestId('carousel-content')
|
|
193
|
+
expect(content).toHaveClass('flex', '-mt-4', 'flex-col')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('applies custom className', () => {
|
|
197
|
+
render(
|
|
198
|
+
<Carousel>
|
|
199
|
+
<CarouselContent className="custom-content" data-testid="content">
|
|
200
|
+
<CarouselItem>Slide</CarouselItem>
|
|
201
|
+
</CarouselContent>
|
|
202
|
+
</Carousel>
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
expect(screen.getByTestId('content')).toHaveClass('custom-content')
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('CarouselItem', () => {
|
|
210
|
+
it('renders with slide accessibility attributes', () => {
|
|
211
|
+
renderCarousel()
|
|
212
|
+
|
|
213
|
+
const item = screen.getByTestId('carousel-item-1')
|
|
214
|
+
expect(item).toHaveAttribute('role', 'group')
|
|
215
|
+
expect(item).toHaveAttribute('aria-roledescription', 'slide')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('applies horizontal item classes by default', () => {
|
|
219
|
+
renderCarousel()
|
|
220
|
+
|
|
221
|
+
const item = screen.getByTestId('carousel-item-1')
|
|
222
|
+
expect(item).toHaveClass('min-w-0', 'shrink-0', 'grow-0', 'basis-full', 'pl-4')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('applies vertical item classes', () => {
|
|
226
|
+
renderCarousel({ orientation: 'vertical' })
|
|
227
|
+
|
|
228
|
+
const item = screen.getByTestId('carousel-item-1')
|
|
229
|
+
expect(item).toHaveClass('pt-4')
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('CarouselPrevious / CarouselNext', () => {
|
|
234
|
+
it('scrolls prev when previous button is clicked', async () => {
|
|
235
|
+
const user = userEvent.setup()
|
|
236
|
+
renderCarousel()
|
|
237
|
+
|
|
238
|
+
await user.click(screen.getByTestId('carousel-previous'))
|
|
239
|
+
expect(mockApi.scrollPrev).toHaveBeenCalledTimes(1)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('scrolls next when next button is clicked', async () => {
|
|
243
|
+
const user = userEvent.setup()
|
|
244
|
+
renderCarousel()
|
|
245
|
+
|
|
246
|
+
await user.click(screen.getByTestId('carousel-next'))
|
|
247
|
+
expect(mockApi.scrollNext).toHaveBeenCalledTimes(1)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('renders screen reader labels', () => {
|
|
251
|
+
renderCarousel()
|
|
252
|
+
|
|
253
|
+
expect(screen.getByText('Previous slide')).toHaveClass('sr-only')
|
|
254
|
+
expect(screen.getByText('Next slide')).toHaveClass('sr-only')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('disables buttons when scrolling is not possible', () => {
|
|
258
|
+
mockApi.canScrollPrev.mockReturnValue(false)
|
|
259
|
+
mockApi.canScrollNext.mockReturnValue(false)
|
|
260
|
+
renderCarousel()
|
|
261
|
+
|
|
262
|
+
expect(screen.getByTestId('carousel-previous')).toBeDisabled()
|
|
263
|
+
expect(screen.getByTestId('carousel-next')).toBeDisabled()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('does not scroll when disabled button is clicked', async () => {
|
|
267
|
+
mockApi.canScrollNext.mockReturnValue(false)
|
|
268
|
+
const user = userEvent.setup()
|
|
269
|
+
renderCarousel()
|
|
270
|
+
|
|
271
|
+
await user.click(screen.getByTestId('carousel-next'))
|
|
272
|
+
expect(mockApi.scrollNext).not.toHaveBeenCalled()
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
describe('useCarousel', () => {
|
|
277
|
+
it('throws when used outside of Carousel', () => {
|
|
278
|
+
const Broken = () => {
|
|
279
|
+
useCarousel()
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// React'in error boundary console çıktısını sustur
|
|
284
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
285
|
+
expect(() => render(<Broken />)).toThrow(
|
|
286
|
+
'useCarousel must be used within a <Carousel />'
|
|
287
|
+
)
|
|
288
|
+
consoleSpy.mockRestore()
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
})
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import {
|
|
4
|
+
InputOTP,
|
|
5
|
+
InputOTPGroup,
|
|
6
|
+
InputOTPSlot,
|
|
7
|
+
InputOTPSeparator,
|
|
8
|
+
inputOTPSlotVariants,
|
|
9
|
+
REGEXP_ONLY_DIGITS,
|
|
10
|
+
} from '../input-otp'
|
|
11
|
+
|
|
12
|
+
// Kontrollü test bileşeni — gerçek input-otp kütüphanesiyle uçtan uca çalışır
|
|
13
|
+
const ControlledOTP = ({
|
|
14
|
+
onChange,
|
|
15
|
+
maxLength = 6,
|
|
16
|
+
...props
|
|
17
|
+
}: {
|
|
18
|
+
onChange?: (value: string) => void
|
|
19
|
+
maxLength?: number
|
|
20
|
+
} & Partial<React.ComponentProps<typeof InputOTP>>) => {
|
|
21
|
+
const [value, setValue] = React.useState('')
|
|
22
|
+
return (
|
|
23
|
+
<InputOTP
|
|
24
|
+
maxLength={maxLength}
|
|
25
|
+
value={value}
|
|
26
|
+
onChange={(newValue) => {
|
|
27
|
+
setValue(newValue)
|
|
28
|
+
onChange?.(newValue)
|
|
29
|
+
}}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<InputOTPGroup data-testid="otp-group">
|
|
33
|
+
{Array.from({ length: maxLength }, (_, i) => (
|
|
34
|
+
<InputOTPSlot key={i} index={i} data-testid={`otp-slot-${i}`} />
|
|
35
|
+
))}
|
|
36
|
+
</InputOTPGroup>
|
|
37
|
+
</InputOTP>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('InputOTP Components', () => {
|
|
42
|
+
describe('InputOTP Root', () => {
|
|
43
|
+
it('renders a textbox with the given maxLength', () => {
|
|
44
|
+
render(<ControlledOTP maxLength={6} />)
|
|
45
|
+
|
|
46
|
+
const input = screen.getByRole('textbox')
|
|
47
|
+
expect(input).toBeInTheDocument()
|
|
48
|
+
expect(input).toHaveAttribute('maxlength', '6')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('applies moonui-theme to the container', () => {
|
|
52
|
+
const { container } = render(<ControlledOTP />)
|
|
53
|
+
|
|
54
|
+
expect(container.querySelector('.moonui-theme')).toBeInTheDocument()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('merges custom containerClassName', () => {
|
|
58
|
+
const { container } = render(
|
|
59
|
+
<ControlledOTP containerClassName="custom-container" />
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const otpContainer = container.querySelector('.custom-container')
|
|
63
|
+
expect(otpContainer).toBeInTheDocument()
|
|
64
|
+
expect(otpContainer).toHaveClass('flex', 'items-center', 'gap-2')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('forwards ref to the underlying input', () => {
|
|
68
|
+
const ref = React.createRef<HTMLInputElement>()
|
|
69
|
+
render(
|
|
70
|
+
<InputOTP ref={ref} maxLength={4}>
|
|
71
|
+
<InputOTPGroup>
|
|
72
|
+
<InputOTPSlot index={0} />
|
|
73
|
+
</InputOTPGroup>
|
|
74
|
+
</InputOTP>
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
expect(ref.current).toBeInstanceOf(HTMLInputElement)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('maintains displayName', () => {
|
|
81
|
+
expect(InputOTP.displayName).toBe('InputOTP')
|
|
82
|
+
expect(InputOTPGroup.displayName).toBe('InputOTPGroup')
|
|
83
|
+
expect(InputOTPSlot.displayName).toBe('InputOTPSlot')
|
|
84
|
+
expect(InputOTPSeparator.displayName).toBe('InputOTPSeparator')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('passes through aria-label to the input', () => {
|
|
88
|
+
render(<ControlledOTP aria-label="One-time password" />)
|
|
89
|
+
|
|
90
|
+
expect(screen.getByRole('textbox')).toHaveAttribute(
|
|
91
|
+
'aria-label',
|
|
92
|
+
'One-time password'
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('disables the input when disabled prop is set', () => {
|
|
97
|
+
render(
|
|
98
|
+
<InputOTP maxLength={4} disabled>
|
|
99
|
+
<InputOTPGroup>
|
|
100
|
+
<InputOTPSlot index={0} />
|
|
101
|
+
</InputOTPGroup>
|
|
102
|
+
</InputOTP>
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
expect(screen.getByRole('textbox')).toBeDisabled()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('Value Entry', () => {
|
|
110
|
+
it('calls onChange and renders characters into slots when typing', () => {
|
|
111
|
+
const handleChange = jest.fn()
|
|
112
|
+
render(<ControlledOTP onChange={handleChange} />)
|
|
113
|
+
|
|
114
|
+
const input = screen.getByRole('textbox')
|
|
115
|
+
fireEvent.change(input, { target: { value: '123' } })
|
|
116
|
+
|
|
117
|
+
expect(handleChange).toHaveBeenLastCalledWith('123')
|
|
118
|
+
expect(screen.getByTestId('otp-slot-0')).toHaveTextContent('1')
|
|
119
|
+
expect(screen.getByTestId('otp-slot-1')).toHaveTextContent('2')
|
|
120
|
+
expect(screen.getByTestId('otp-slot-2')).toHaveTextContent('3')
|
|
121
|
+
expect(screen.getByTestId('otp-slot-3')).toHaveTextContent('')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('supports pasting a full code at once', () => {
|
|
125
|
+
const handleChange = jest.fn()
|
|
126
|
+
render(<ControlledOTP onChange={handleChange} />)
|
|
127
|
+
|
|
128
|
+
// Paste davranışı: input değeri tek seferde tamamen değişir
|
|
129
|
+
const input = screen.getByRole('textbox')
|
|
130
|
+
fireEvent.change(input, { target: { value: '123456' } })
|
|
131
|
+
|
|
132
|
+
expect(handleChange).toHaveBeenLastCalledWith('123456')
|
|
133
|
+
expect(screen.getByTestId('otp-slot-5')).toHaveTextContent('6')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('does not exceed maxLength', () => {
|
|
137
|
+
const handleChange = jest.fn()
|
|
138
|
+
render(<ControlledOTP maxLength={4} onChange={handleChange} />)
|
|
139
|
+
|
|
140
|
+
const input = screen.getByRole('textbox')
|
|
141
|
+
fireEvent.change(input, { target: { value: '12345' } })
|
|
142
|
+
|
|
143
|
+
// input-otp maxLength üstünü keser veya reddeder — 5 karakterlik değer asla yayılmaz
|
|
144
|
+
expect(handleChange).not.toHaveBeenCalledWith('12345')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('rejects non-matching characters when pattern is digits-only', () => {
|
|
148
|
+
const handleChange = jest.fn()
|
|
149
|
+
render(<ControlledOTP pattern={REGEXP_ONLY_DIGITS} onChange={handleChange} />)
|
|
150
|
+
|
|
151
|
+
const input = screen.getByRole('textbox')
|
|
152
|
+
fireEvent.change(input, { target: { value: 'abc' } })
|
|
153
|
+
|
|
154
|
+
expect(handleChange).not.toHaveBeenCalled()
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('InputOTPSlot', () => {
|
|
159
|
+
it('renders base styles with token-based border classes', () => {
|
|
160
|
+
render(<ControlledOTP />)
|
|
161
|
+
|
|
162
|
+
const slot = screen.getByTestId('otp-slot-0')
|
|
163
|
+
expect(slot).toHaveClass('border-y', 'border-r', 'border-input')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('applies custom className', () => {
|
|
167
|
+
render(
|
|
168
|
+
<InputOTP maxLength={2}>
|
|
169
|
+
<InputOTPGroup>
|
|
170
|
+
<InputOTPSlot index={0} className="custom-slot" data-testid="slot" />
|
|
171
|
+
</InputOTPGroup>
|
|
172
|
+
</InputOTP>
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
expect(screen.getByTestId('slot')).toHaveClass('custom-slot')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('marks the active slot with data-active and ring styles on focus', () => {
|
|
179
|
+
render(<ControlledOTP />)
|
|
180
|
+
|
|
181
|
+
const input = screen.getByRole('textbox')
|
|
182
|
+
fireEvent.focus(input)
|
|
183
|
+
|
|
184
|
+
const slot = screen.getByTestId('otp-slot-0')
|
|
185
|
+
expect(slot).toHaveAttribute('data-active')
|
|
186
|
+
expect(slot).toHaveClass('ring-2', 'ring-ring')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('exposes active ring styling through inputOTPSlotVariants', () => {
|
|
190
|
+
// Aktif slot vurgusu token tabanlı ring sınıflarını içermeli
|
|
191
|
+
expect(inputOTPSlotVariants({ isActive: true })).toContain('ring-2')
|
|
192
|
+
expect(inputOTPSlotVariants({ isActive: true })).toContain('ring-ring')
|
|
193
|
+
expect(inputOTPSlotVariants({ isActive: false })).not.toContain('ring-2')
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('InputOTPSeparator', () => {
|
|
198
|
+
it('renders with separator role', () => {
|
|
199
|
+
render(
|
|
200
|
+
<InputOTP maxLength={4}>
|
|
201
|
+
<InputOTPGroup>
|
|
202
|
+
<InputOTPSlot index={0} />
|
|
203
|
+
</InputOTPGroup>
|
|
204
|
+
<InputOTPSeparator data-testid="separator" />
|
|
205
|
+
<InputOTPGroup>
|
|
206
|
+
<InputOTPSlot index={1} />
|
|
207
|
+
</InputOTPGroup>
|
|
208
|
+
</InputOTP>
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const separator = screen.getByTestId('separator')
|
|
212
|
+
expect(separator).toHaveAttribute('role', 'separator')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('applies custom className', () => {
|
|
216
|
+
render(
|
|
217
|
+
<InputOTP maxLength={2}>
|
|
218
|
+
<InputOTPGroup>
|
|
219
|
+
<InputOTPSlot index={0} />
|
|
220
|
+
</InputOTPGroup>
|
|
221
|
+
<InputOTPSeparator className="custom-separator" data-testid="separator" />
|
|
222
|
+
</InputOTP>
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
expect(screen.getByTestId('separator')).toHaveClass('custom-separator')
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import '@testing-library/jest-dom'
|
|
4
|
+
import { Kbd } from '../kbd'
|
|
5
|
+
|
|
6
|
+
describe('Kbd Component', () => {
|
|
7
|
+
it('renders a <kbd> element with children', () => {
|
|
8
|
+
render(<Kbd>K</Kbd>)
|
|
9
|
+
const kbd = screen.getByText('K')
|
|
10
|
+
expect(kbd).toBeInTheDocument()
|
|
11
|
+
expect(kbd.tagName).toBe('KBD')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('applies base keyboard-key styles (token based)', () => {
|
|
15
|
+
render(<Kbd>Ctrl</Kbd>)
|
|
16
|
+
const kbd = screen.getByText('Ctrl')
|
|
17
|
+
expect(kbd).toHaveClass('inline-flex')
|
|
18
|
+
expect(kbd).toHaveClass('items-center')
|
|
19
|
+
expect(kbd).toHaveClass('border-border')
|
|
20
|
+
expect(kbd).toHaveClass('bg-muted')
|
|
21
|
+
expect(kbd).toHaveClass('text-muted-foreground')
|
|
22
|
+
expect(kbd).toHaveClass('font-mono')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('applies custom className', () => {
|
|
26
|
+
render(<Kbd className="custom-kbd">Esc</Kbd>)
|
|
27
|
+
expect(screen.getByText('Esc')).toHaveClass('custom-kbd')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('Sizes', () => {
|
|
31
|
+
it('renders medium size by default', () => {
|
|
32
|
+
render(<Kbd>Enter</Kbd>)
|
|
33
|
+
const kbd = screen.getByText('Enter')
|
|
34
|
+
expect(kbd).toHaveClass('h-6')
|
|
35
|
+
expect(kbd).toHaveClass('min-w-6')
|
|
36
|
+
expect(kbd).toHaveClass('text-xs')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders small size correctly', () => {
|
|
40
|
+
render(<Kbd size="sm">Shift</Kbd>)
|
|
41
|
+
const kbd = screen.getByText('Shift')
|
|
42
|
+
expect(kbd).toHaveClass('h-5')
|
|
43
|
+
expect(kbd).toHaveClass('min-w-5')
|
|
44
|
+
expect(kbd).toHaveClass('text-[10px]')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('renders medium size explicitly', () => {
|
|
48
|
+
render(<Kbd size="md">Tab</Kbd>)
|
|
49
|
+
const kbd = screen.getByText('Tab')
|
|
50
|
+
expect(kbd).toHaveClass('h-6')
|
|
51
|
+
expect(kbd).toHaveClass('min-w-6')
|
|
52
|
+
expect(kbd).toHaveClass('text-xs')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('Children content', () => {
|
|
57
|
+
it('renders symbol content visibly', () => {
|
|
58
|
+
render(<Kbd>⌘</Kbd>)
|
|
59
|
+
expect(screen.getByText('⌘')).toBeInTheDocument()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('renders multi-character content visibly', () => {
|
|
63
|
+
render(<Kbd>Ctrl</Kbd>)
|
|
64
|
+
expect(screen.getByText('Ctrl')).toBeInTheDocument()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('Ref forwarding', () => {
|
|
69
|
+
it('forwards ref to the kbd element', () => {
|
|
70
|
+
const ref = React.createRef<HTMLElement>()
|
|
71
|
+
render(<Kbd ref={ref}>K</Kbd>)
|
|
72
|
+
expect(ref.current).toBeInstanceOf(HTMLElement)
|
|
73
|
+
expect(ref.current?.tagName).toBe('KBD')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('maintains displayName', () => {
|
|
78
|
+
expect(Kbd.displayName).toBe('Kbd')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('passes through HTML attributes', () => {
|
|
82
|
+
render(<Kbd data-testid="kbd-test" id="kbd-1">K</Kbd>)
|
|
83
|
+
const kbd = screen.getByTestId('kbd-test')
|
|
84
|
+
expect(kbd).toHaveAttribute('id', 'kbd-1')
|
|
85
|
+
})
|
|
86
|
+
})
|