@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
@@ -1,7 +1,25 @@
1
1
  import React from 'react'
2
- import { render, screen, fireEvent, waitFor } from '@testing-library/react'
2
+ import { render as rtlRender, screen, fireEvent, waitFor, within } from '@testing-library/react'
3
3
  import userEvent from '@testing-library/user-event'
4
- import { Tooltip } from '../tooltip'
4
+ import { SimpleTooltip as Tooltip, TooltipProvider } from '../tooltip'
5
+
6
+ // GERÇEK API NOTLARI (kaynak: ../tooltip.tsx — Radix `@radix-ui/react-tooltip` üzerine kurulu)
7
+ //
8
+ // 1) Testler `<Tooltip content=... >` API'sini kullanıyor; bu `SimpleTooltip`in yüzeyi.
9
+ // Dışa açılan `Tooltip` ise ham `TooltipPrimitive.Root` — `content` prop'u yok.
10
+ // 2) Radix, Tooltip'in bir `TooltipProvider` atası altında olmasını ZORUNLU kılar
11
+ // ("`Tooltip` must be used within `TooltipProvider`") ve SimpleTooltip kendi
12
+ // sağlayıcısını taşımaz. Tüm render'lar sağlayıcıyla sarılır.
13
+ // 3) Radix, stilli içerik kutusunun İÇİNE `role="tooltip"` taşıyan görsel-gizli bir
14
+ // `span` koyar (erişilebilir isim buradan okunur). Yani `getByRole('tooltip')`
15
+ // sr-only span'i döndürür; CVA sınıflarını taşıyan kutu onun EBEVEYNİdir
16
+ // (`getTooltipContent`). İçerik metni DOM'a iki kez düşer (kutu + sr-only kopya),
17
+ // bu yüzden tekil `getByText` sorguları "multiple elements" ile patlar.
18
+ // 4) Gerçek varyant/boyut listesi `tooltipVariants` CVA'sından gelir:
19
+ // variant: default|secondary|success|warning|destructive|info, size: sm|md|lg.
20
+ // `radius`, `shadow`, `primary`, `caution` gibi prop/değerler API'de YOKTUR —
21
+ // yuvarlaklık ve gölge temel (base) sınıflardan gelir, `className` ile ezilir.
22
+ // 5) `sideOffset` Radix Popper'a gider ve konumu sarmalayıcı `transform`una yansır.
5
23
 
6
24
  // Mock next-themes
7
25
  jest.mock('next-themes', () => ({
@@ -10,10 +28,38 @@ jest.mock('next-themes', () => ({
10
28
  }),
11
29
  }))
12
30
 
13
-
31
+ const render: typeof rtlRender = (ui: any, options?: any) =>
32
+ rtlRender(<TooltipProvider>{ui}</TooltipProvider>, options)
33
+
34
+ // CVA sınıflarını taşıyan görünür içerik kutusu (sr-only tooltip span'inin ebeveyni)
35
+ const getTooltipContent = () => screen.getByRole('tooltip').parentElement as HTMLElement
36
+
37
+ // Radix Popper'ın konumlandırma sarmalayıcısı (transform burada uygulanır)
38
+ const getPopperWrapper = () => getTooltipContent().parentElement as HTMLElement
39
+
40
+ // Tooltip'i hover ile açar ve açılmasını bekler
41
+ const openByHover = async (
42
+ user: ReturnType<typeof userEvent.setup>,
43
+ trigger: HTMLElement
44
+ ) => {
45
+ await user.hover(trigger)
46
+ await screen.findByRole('tooltip')
47
+ }
48
+
49
+ // Radix varsayılan olarak "hoverable content" kullanır: tetikleyiciden çıkmak tek
50
+ // başına kapatmaz, imlecin tetikleyici+içerik arasındaki grace-area'nın DIŞINA
51
+ // gerçekten hareket etmesi beklenir (kullanıcı tooltip'in üzerine gidebilsin diye).
52
+ // jsdom'da tüm rect'ler 0 olduğundan bu hareketi açıkça üretmek gerekir.
53
+ const movePointerAway = async (
54
+ user: ReturnType<typeof userEvent.setup>,
55
+ trigger: HTMLElement
56
+ ) => {
57
+ await user.unhover(trigger)
58
+ fireEvent.pointerMove(document.body, { clientX: 400, clientY: 400 })
59
+ }
14
60
 
15
61
  describe('Tooltip Components', () => {
16
- describe('Tooltip Component', () => {
62
+ describe('SimpleTooltip Component', () => {
17
63
  describe('Basic Rendering', () => {
18
64
  it('renders correctly with default props', () => {
19
65
  render(
@@ -21,9 +67,11 @@ describe('Tooltip Components', () => {
21
67
  <button>Hover me</button>
22
68
  </Tooltip>
23
69
  )
24
-
70
+
25
71
  expect(screen.getByRole('button')).toBeInTheDocument()
26
72
  expect(screen.getByText('Hover me')).toBeInTheDocument()
73
+ // Kapalıyken içerik DOM'da olmamalı
74
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
27
75
  })
28
76
 
29
77
  it('shows tooltip on hover', async () => {
@@ -33,14 +81,15 @@ describe('Tooltip Components', () => {
33
81
  <button>Hover me</button>
34
82
  </Tooltip>
35
83
  )
36
-
84
+
37
85
  const trigger = screen.getByRole('button')
38
86
  await user.hover(trigger)
39
-
87
+
40
88
  await waitFor(() => {
41
89
  expect(screen.getByRole('tooltip')).toBeInTheDocument()
42
- expect(screen.getByText('Test tooltip')).toBeInTheDocument()
43
90
  })
91
+ expect(screen.getByRole('tooltip')).toHaveTextContent('Test tooltip')
92
+ expect(getTooltipContent()).toHaveTextContent('Test tooltip')
44
93
  })
45
94
 
46
95
  it('hides tooltip on mouse leave', async () => {
@@ -50,131 +99,181 @@ describe('Tooltip Components', () => {
50
99
  <button>Hover me</button>
51
100
  </Tooltip>
52
101
  )
53
-
102
+
54
103
  const trigger = screen.getByRole('button')
55
104
  await user.hover(trigger)
56
-
105
+
57
106
  await waitFor(() => {
58
107
  expect(screen.getByRole('tooltip')).toBeInTheDocument()
59
108
  })
60
-
61
- await user.unhover(trigger)
62
-
109
+
110
+ await movePointerAway(user, trigger)
111
+
63
112
  await waitFor(() => {
64
113
  expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
65
114
  })
66
115
  })
67
116
 
68
- it('applies custom className', () => {
117
+ it('links trigger to tooltip via aria-describedby', async () => {
118
+ const user = userEvent.setup()
69
119
  render(
70
- <Tooltip content="Test tooltip" className="custom-tooltip">
120
+ <Tooltip content="Test tooltip" delayDuration={0}>
71
121
  <button>Hover me</button>
72
122
  </Tooltip>
73
123
  )
74
-
75
- const wrapper = screen.getByRole('button').parentElement
76
- expect(wrapper).toHaveClass('relative', 'inline-block')
124
+
125
+ const trigger = screen.getByRole('button')
126
+ expect(trigger).not.toHaveAttribute('aria-describedby')
127
+
128
+ await openByHover(user, trigger)
129
+
130
+ // Açıkken tetikleyici, sr-only tooltip düğümünü tarif eder
131
+ expect(trigger).toHaveAttribute('aria-describedby', screen.getByRole('tooltip').id)
132
+ expect(trigger).toHaveAttribute('data-state', 'delayed-open')
133
+ })
134
+
135
+ it('applies custom className', async () => {
136
+ const user = userEvent.setup()
137
+ render(
138
+ <Tooltip content="Test tooltip" className="custom-tooltip" delayDuration={0}>
139
+ <button>Hover me</button>
140
+ </Tooltip>
141
+ )
142
+
143
+ await openByHover(user, screen.getByRole('button'))
144
+
145
+ // className TooltipContent'e iner (SimpleTooltip → TooltipContent)
146
+ expect(getTooltipContent()).toHaveClass('custom-tooltip')
77
147
  })
78
148
 
79
149
  it('has correct displayName', () => {
80
- expect(Tooltip.displayName || Tooltip.name || 'Tooltip').toBe('Tooltip')
150
+ expect(Tooltip.displayName).toBe('SimpleTooltip')
81
151
  })
82
152
  })
83
153
 
84
154
  describe('Variants', () => {
85
- const variants = ['primary', 'secondary', 'success', 'caution', 'destructive', 'info'] as const
86
-
87
- variants.forEach((variant) => {
155
+ // tooltipVariants CVA'sındaki gerçek varyantlar ve ürettikleri sınıflar
156
+ const variants = [
157
+ { variant: 'default', className: 'bg-primary', textClassName: 'text-primary-foreground' },
158
+ { variant: 'secondary', className: 'bg-secondary', textClassName: 'text-secondary-foreground' },
159
+ { variant: 'success', className: 'bg-green-500', textClassName: 'text-white' },
160
+ { variant: 'warning', className: 'bg-yellow-500', textClassName: 'text-white' },
161
+ { variant: 'destructive', className: 'bg-destructive', textClassName: 'text-destructive-foreground' },
162
+ { variant: 'info', className: 'bg-blue-500', textClassName: 'text-white' },
163
+ ] as const
164
+
165
+ variants.forEach(({ variant, className, textClassName }) => {
88
166
  it(`renders ${variant} variant correctly`, async () => {
89
167
  const user = userEvent.setup()
90
168
  render(
91
- <Tooltip content="Test tooltip" variant={variant}>
169
+ <Tooltip content="Test tooltip" variant={variant} delayDuration={0}>
92
170
  <button>Hover me</button>
93
171
  </Tooltip>
94
172
  )
95
-
96
- const trigger = screen.getByRole('button')
97
- await user.hover(trigger)
98
-
99
- await waitFor(() => {
100
- const tooltip = screen.getByRole('tooltip')
101
- expect(tooltip).toBeInTheDocument()
102
- expect(tooltip).toHaveClass(`bg-${variant}`)
103
- })
173
+
174
+ await openByHover(user, screen.getByRole('button'))
175
+
176
+ const tooltip = getTooltipContent()
177
+ expect(tooltip).toBeInTheDocument()
178
+ expect(tooltip).toHaveClass(className)
179
+ expect(tooltip).toHaveClass(textClassName)
104
180
  })
105
181
  })
106
182
  })
107
183
 
108
184
  describe('Sizes', () => {
109
- const sizes = ['sm', 'md', 'lg'] as const
110
-
111
- sizes.forEach((size) => {
185
+ // Boyut sınıfları tooltipVariants üzerinden gelir (twMerge base padding'i ezer)
186
+ const sizes = [
187
+ { size: 'sm', classes: ['px-2', 'py-1', 'text-xs'] },
188
+ { size: 'md', classes: ['px-3', 'py-1.5', 'text-sm'] },
189
+ { size: 'lg', classes: ['px-4', 'py-2', 'text-base'] },
190
+ ] as const
191
+
192
+ sizes.forEach(({ size, classes }) => {
112
193
  it(`renders ${size} size correctly`, async () => {
113
194
  const user = userEvent.setup()
114
195
  render(
115
- <Tooltip content="Test tooltip" size={size}>
196
+ <Tooltip content="Test tooltip" size={size} delayDuration={0}>
116
197
  <button>Hover me</button>
117
198
  </Tooltip>
118
199
  )
119
-
120
- const trigger = screen.getByRole('button')
121
- await user.hover(trigger)
122
-
123
- await waitFor(() => {
124
- const tooltip = screen.getByRole('tooltip')
125
- expect(tooltip).toBeInTheDocument()
126
- // Size classes are applied through tooltipVariants
127
- expect(tooltip).toHaveAttribute('class')
128
- })
200
+
201
+ await openByHover(user, screen.getByRole('button'))
202
+
203
+ const tooltip = getTooltipContent()
204
+ expect(tooltip).toBeInTheDocument()
205
+ classes.forEach((cls) => expect(tooltip).toHaveClass(cls))
129
206
  })
130
207
  })
131
208
  })
132
209
 
133
- describe('Radius', () => {
134
- const radiusOptions = ['none', 'sm', 'md', 'lg', 'full'] as const
210
+ describe('Base Styling', () => {
211
+ // `radius` / `shadow` prop'ları YOK; yuvarlaklık, kenarlık ve gölge base sınıflarda sabit
212
+ it('applies base radius, border, shadow and layering classes', async () => {
213
+ const user = userEvent.setup()
214
+ render(
215
+ <Tooltip content="Test tooltip" delayDuration={0}>
216
+ <button>Hover me</button>
217
+ </Tooltip>
218
+ )
135
219
 
136
- radiusOptions.forEach((radius) => {
137
- it(`renders ${radius} radius correctly`, async () => {
138
- const user = userEvent.setup()
139
- render(
140
- <Tooltip content="Test tooltip" radius={radius}>
141
- <button>Hover me</button>
142
- </Tooltip>
143
- )
144
-
145
- const trigger = screen.getByRole('button')
146
- await user.hover(trigger)
147
-
148
- await waitFor(() => {
149
- const tooltip = screen.getByRole('tooltip')
150
- expect(tooltip).toBeInTheDocument()
151
- expect(tooltip).toHaveClass(`rounded-${radius === 'none' ? 'none' : radius}`)
152
- })
153
- })
220
+ await openByHover(user, screen.getByRole('button'))
221
+
222
+ const tooltip = getTooltipContent()
223
+ expect(tooltip).toHaveClass('rounded-md')
224
+ expect(tooltip).toHaveClass('border')
225
+ expect(tooltip).toHaveClass('shadow-md')
226
+ expect(tooltip).toHaveClass('z-50')
227
+ expect(tooltip).toHaveClass('overflow-hidden')
228
+ expect(tooltip).toHaveClass('moonui-theme')
229
+ })
230
+
231
+ it('lets className override radius and shadow tokens', async () => {
232
+ const user = userEvent.setup()
233
+ render(
234
+ <Tooltip content="Test tooltip" className="rounded-none shadow-none" delayDuration={0}>
235
+ <button>Hover me</button>
236
+ </Tooltip>
237
+ )
238
+
239
+ await openByHover(user, screen.getByRole('button'))
240
+
241
+ // cn() = twMerge → çakışan token'lar className lehine çözülür
242
+ const tooltip = getTooltipContent()
243
+ expect(tooltip).toHaveClass('rounded-none')
244
+ expect(tooltip).toHaveClass('shadow-none')
245
+ expect(tooltip).not.toHaveClass('rounded-md')
246
+ expect(tooltip).not.toHaveClass('shadow-md')
154
247
  })
155
248
  })
156
249
 
157
- describe('Shadow', () => {
158
- const shadowOptions = ['none', 'sm', 'md', 'lg'] as const
250
+ describe('Arrow', () => {
251
+ it('does not render arrow by default', async () => {
252
+ const user = userEvent.setup()
253
+ render(
254
+ <Tooltip content="Test tooltip" delayDuration={0}>
255
+ <button>Hover me</button>
256
+ </Tooltip>
257
+ )
159
258
 
160
- shadowOptions.forEach((shadow) => {
161
- it(`renders ${shadow} shadow correctly`, async () => {
162
- const user = userEvent.setup()
163
- render(
164
- <Tooltip content="Test tooltip" shadow={shadow}>
165
- <button>Hover me</button>
166
- </Tooltip>
167
- )
168
-
169
- const trigger = screen.getByRole('button')
170
- await user.hover(trigger)
171
-
172
- await waitFor(() => {
173
- const tooltip = screen.getByRole('tooltip')
174
- expect(tooltip).toBeInTheDocument()
175
- expect(tooltip).toHaveClass(`shadow-${shadow}`)
176
- })
177
- })
259
+ await openByHover(user, screen.getByRole('button'))
260
+
261
+ expect(getTooltipContent().querySelector('svg')).toBeNull()
262
+ })
263
+
264
+ it('renders arrow when showArrow is true', async () => {
265
+ const user = userEvent.setup()
266
+ render(
267
+ <Tooltip content="Test tooltip" showArrow delayDuration={0}>
268
+ <button>Hover me</button>
269
+ </Tooltip>
270
+ )
271
+
272
+ await openByHover(user, screen.getByRole('button'))
273
+
274
+ const arrow = getTooltipContent().querySelector('svg')
275
+ expect(arrow).not.toBeNull()
276
+ expect(arrow).toHaveClass('fill-current')
178
277
  })
179
278
  })
180
279
 
@@ -185,20 +284,20 @@ describe('Tooltip Components', () => {
185
284
  it(`renders ${side} position correctly`, async () => {
186
285
  const user = userEvent.setup()
187
286
  render(
188
- <Tooltip content="Test tooltip" side={side}>
287
+ // jsdom'da görünür alan 0×0'dır; çarpışma kaçınması açık kalırsa Radix
288
+ // istenen tarafı ters çevirir. İstenen tarafın gerçekten iletildiğini
289
+ // ölçmek için collision davranışı kapatılır.
290
+ <Tooltip content="Test tooltip" side={side} delayDuration={0} avoidCollisions={false}>
189
291
  <button>Hover me</button>
190
292
  </Tooltip>
191
293
  )
192
-
193
- const trigger = screen.getByRole('button')
194
- await user.hover(trigger)
195
-
196
- await waitFor(() => {
197
- const tooltip = screen.getByRole('tooltip')
198
- expect(tooltip).toBeInTheDocument()
199
- // Position classes are applied through sideClasses
200
- expect(tooltip).toHaveAttribute('class')
201
- })
294
+
295
+ await openByHover(user, screen.getByRole('button'))
296
+
297
+ const tooltip = getTooltipContent()
298
+ expect(tooltip).toBeInTheDocument()
299
+ // Konum Radix tarafından data-side ile yayınlanır (stil sınıfları buna bağlanır)
300
+ expect(tooltip).toHaveAttribute('data-side', side)
202
301
  })
203
302
  })
204
303
 
@@ -208,20 +307,16 @@ describe('Tooltip Components', () => {
208
307
  it(`renders ${align} alignment correctly`, async () => {
209
308
  const user = userEvent.setup()
210
309
  render(
211
- <Tooltip content="Test tooltip" align={align}>
310
+ <Tooltip content="Test tooltip" align={align} delayDuration={0}>
212
311
  <button>Hover me</button>
213
312
  </Tooltip>
214
313
  )
215
-
216
- const trigger = screen.getByRole('button')
217
- await user.hover(trigger)
218
-
219
- await waitFor(() => {
220
- const tooltip = screen.getByRole('tooltip')
221
- expect(tooltip).toBeInTheDocument()
222
- // Alignment classes are applied through alignClasses
223
- expect(tooltip).toHaveAttribute('class')
224
- })
314
+
315
+ await openByHover(user, screen.getByRole('button'))
316
+
317
+ const tooltip = getTooltipContent()
318
+ expect(tooltip).toBeInTheDocument()
319
+ expect(tooltip).toHaveAttribute('data-align', align)
225
320
  })
226
321
  })
227
322
  })
@@ -234,10 +329,10 @@ describe('Tooltip Components', () => {
234
329
  <button>Hover me</button>
235
330
  </Tooltip>
236
331
  )
237
-
332
+
238
333
  const trigger = screen.getByRole('button')
239
334
  await user.hover(trigger)
240
-
335
+
241
336
  // Should appear after 100ms
242
337
  await waitFor(() => {
243
338
  expect(screen.getByRole('tooltip')).toBeInTheDocument()
@@ -251,10 +346,10 @@ describe('Tooltip Components', () => {
251
346
  <button>Hover me</button>
252
347
  </Tooltip>
253
348
  )
254
-
349
+
255
350
  const trigger = screen.getByRole('button')
256
351
  await user.hover(trigger)
257
-
352
+
258
353
  // Should appear immediately
259
354
  await waitFor(() => {
260
355
  expect(screen.getByRole('tooltip')).toBeInTheDocument()
@@ -266,19 +361,28 @@ describe('Tooltip Components', () => {
266
361
  it('applies custom side offset', async () => {
267
362
  const user = userEvent.setup()
268
363
  render(
269
- <Tooltip content="Test tooltip" sideOffset={10}>
364
+ <Tooltip content="Test tooltip" sideOffset={10} delayDuration={0}>
270
365
  <button>Hover me</button>
271
366
  </Tooltip>
272
367
  )
273
-
274
- const trigger = screen.getByRole('button')
275
- await user.hover(trigger)
276
-
277
- await waitFor(() => {
278
- const tooltip = screen.getByRole('tooltip')
279
- expect(tooltip).toBeInTheDocument()
280
- expect(tooltip).toHaveStyle({ marginBottom: '10px' })
281
- })
368
+
369
+ await openByHover(user, screen.getByRole('button'))
370
+
371
+ // sideOffset Popper'a gider: side="top" için içerik 10px yukarı ötelenir
372
+ expect(getPopperWrapper()).toHaveStyle({ transform: 'translate(0px, -10px)' })
373
+ })
374
+
375
+ it('applies zero side offset', async () => {
376
+ const user = userEvent.setup()
377
+ render(
378
+ <Tooltip content="Test tooltip" sideOffset={0} delayDuration={0}>
379
+ <button>Hover me</button>
380
+ </Tooltip>
381
+ )
382
+
383
+ await openByHover(user, screen.getByRole('button'))
384
+
385
+ expect(getPopperWrapper()).toHaveStyle({ transform: 'translate(0px, 0px)' })
282
386
  })
283
387
  })
284
388
 
@@ -286,33 +390,31 @@ describe('Tooltip Components', () => {
286
390
  it('renders text content', async () => {
287
391
  const user = userEvent.setup()
288
392
  render(
289
- <Tooltip content="Simple text">
393
+ <Tooltip content="Simple text" delayDuration={0}>
290
394
  <button>Hover me</button>
291
395
  </Tooltip>
292
396
  )
293
-
294
- const trigger = screen.getByRole('button')
295
- await user.hover(trigger)
296
-
297
- await waitFor(() => {
298
- expect(screen.getByText('Simple text')).toBeInTheDocument()
299
- })
397
+
398
+ await openByHover(user, screen.getByRole('button'))
399
+
400
+ expect(getTooltipContent()).toHaveTextContent('Simple text')
401
+ expect(screen.getByRole('tooltip')).toHaveTextContent('Simple text')
300
402
  })
301
403
 
302
404
  it('renders JSX content', async () => {
303
405
  const user = userEvent.setup()
304
406
  render(
305
- <Tooltip content={<div data-testid="jsx-content">JSX Content</div>}>
407
+ <Tooltip content={<div data-testid="jsx-content">JSX Content</div>} delayDuration={0}>
306
408
  <button>Hover me</button>
307
409
  </Tooltip>
308
410
  )
309
-
310
- const trigger = screen.getByRole('button')
311
- await user.hover(trigger)
312
-
313
- await waitFor(() => {
314
- expect(screen.getByTestId('jsx-content')).toBeInTheDocument()
315
- })
411
+
412
+ await openByHover(user, screen.getByRole('button'))
413
+
414
+ const tooltip = getTooltipContent()
415
+ // Radix içeriği sr-only kopyada da render eder → ilk düğüm görünür olandır
416
+ expect(within(tooltip).getAllByTestId('jsx-content')[0]).toBeInTheDocument()
417
+ expect(tooltip).toHaveTextContent('JSX Content')
316
418
  })
317
419
 
318
420
  it('renders complex content', async () => {
@@ -323,20 +425,18 @@ describe('Tooltip Components', () => {
323
425
  <p>Description</p>
324
426
  </div>
325
427
  )
326
-
428
+
327
429
  render(
328
- <Tooltip content={complexContent}>
430
+ <Tooltip content={complexContent} delayDuration={0}>
329
431
  <button>Hover me</button>
330
432
  </Tooltip>
331
433
  )
332
-
333
- const trigger = screen.getByRole('button')
334
- await user.hover(trigger)
335
-
336
- await waitFor(() => {
337
- expect(screen.getByText('Title')).toBeInTheDocument()
338
- expect(screen.getByText('Description')).toBeInTheDocument()
339
- })
434
+
435
+ await openByHover(user, screen.getByRole('button'))
436
+
437
+ const tooltip = getTooltipContent()
438
+ expect(within(tooltip).getAllByText('Title')[0]).toBeInTheDocument()
439
+ expect(within(tooltip).getAllByText('Description')[0]).toBeInTheDocument()
340
440
  })
341
441
  })
342
442
 
@@ -347,10 +447,10 @@ describe('Tooltip Components', () => {
347
447
  <button>Focus me</button>
348
448
  </Tooltip>
349
449
  )
350
-
450
+
351
451
  const trigger = screen.getByRole('button')
352
452
  fireEvent.focus(trigger)
353
-
453
+
354
454
  await waitFor(() => {
355
455
  expect(screen.getByRole('tooltip')).toBeInTheDocument()
356
456
  })
@@ -362,87 +462,58 @@ describe('Tooltip Components', () => {
362
462
  <button>Focus me</button>
363
463
  </Tooltip>
364
464
  )
365
-
465
+
366
466
  const trigger = screen.getByRole('button')
367
467
  fireEvent.focus(trigger)
368
-
468
+
369
469
  await waitFor(() => {
370
470
  expect(screen.getByRole('tooltip')).toBeInTheDocument()
371
471
  })
372
-
472
+
373
473
  fireEvent.blur(trigger)
374
-
474
+
375
475
  await waitFor(() => {
376
476
  expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
377
477
  })
378
478
  })
379
479
  })
380
480
 
381
- describe('Complex Combinations', () => {
382
- it('renders with all props', async () => {
481
+ describe('Controlled State', () => {
482
+ it('renders open when defaultOpen is set and reports changes', async () => {
483
+ const onOpenChange = jest.fn()
383
484
  const user = userEvent.setup()
384
485
  render(
385
- <Tooltip
386
- content="Complex tooltip"
387
- variant="success"
388
- size="lg"
389
- radius="lg"
390
- shadow="lg"
391
- side="bottom"
392
- align="end"
393
- delayDuration={50}
394
- sideOffset={8}
395
- className="custom-tooltip"
396
- >
397
- <button>Complex button</button>
486
+ <Tooltip content="Test tooltip" defaultOpen onOpenChange={onOpenChange}>
487
+ <button>Hover me</button>
398
488
  </Tooltip>
399
489
  )
400
-
401
- const trigger = screen.getByRole('button')
402
- await user.hover(trigger)
403
-
490
+
491
+ expect(screen.getByRole('tooltip')).toBeInTheDocument()
492
+
493
+ await user.hover(screen.getByRole('button'))
494
+ await movePointerAway(user, screen.getByRole('button'))
495
+
404
496
  await waitFor(() => {
405
- const tooltip = screen.getByRole('tooltip')
406
- expect(tooltip).toBeInTheDocument()
407
- expect(tooltip).toHaveClass('bg-success')
408
- expect(tooltip).toHaveClass('rounded-lg')
409
- expect(tooltip).toHaveClass('shadow-lg')
410
- expect(tooltip).toHaveStyle({ marginTop: '8px' })
497
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
411
498
  })
499
+ expect(onOpenChange).toHaveBeenCalledWith(false)
412
500
  })
413
501
 
414
- it('works with different trigger elements', async () => {
502
+ it('stays open while controlled open is true', async () => {
415
503
  const user = userEvent.setup()
416
504
  render(
417
- <div>
418
- <Tooltip content="Button tooltip">
419
- <button data-testid="tooltip-button">Button</button>
420
- </Tooltip>
421
- <Tooltip content="Link tooltip">
422
- <a href="#" role="link">Link</a>
423
- </Tooltip>
424
- <Tooltip content="Div tooltip">
425
- <div role="button" tabIndex={0}>Div</div>
426
- </Tooltip>
427
- </div>
505
+ <Tooltip content="Test tooltip" open>
506
+ <button>Hover me</button>
507
+ </Tooltip>
428
508
  )
429
-
430
- // Test button
431
- await user.hover(screen.getByTestId('tooltip-button'))
432
- await waitFor(() => {
433
- expect(screen.getByText('Button tooltip')).toBeInTheDocument()
434
- })
435
-
436
- await user.unhover(screen.getByTestId('tooltip-button'))
437
- await waitFor(() => {
438
- expect(screen.queryByText('Button tooltip')).not.toBeInTheDocument()
439
- })
440
-
441
- // Test link
442
- await user.hover(screen.getByRole('link'))
443
- await waitFor(() => {
444
- expect(screen.getByText('Link tooltip')).toBeInTheDocument()
445
- })
509
+
510
+ expect(screen.getByRole('tooltip')).toBeInTheDocument()
511
+
512
+ await user.hover(screen.getByRole('button'))
513
+ await movePointerAway(user, screen.getByRole('button'))
514
+
515
+ // Kontrollü `open` dışarıdan yönetilir; iç etkileşim kapatamaz
516
+ expect(screen.getByRole('tooltip')).toBeInTheDocument()
446
517
  })
447
518
  })
448
519
 
@@ -450,57 +521,46 @@ describe('Tooltip Components', () => {
450
521
  it('handles empty content', async () => {
451
522
  const user = userEvent.setup()
452
523
  render(
453
- <Tooltip content="">
524
+ <Tooltip content="" delayDuration={0}>
454
525
  <button>Hover me</button>
455
526
  </Tooltip>
456
527
  )
457
-
458
- const trigger = screen.getByRole('button')
459
- await user.hover(trigger)
460
-
461
- await waitFor(() => {
462
- const tooltip = screen.getByRole('tooltip')
463
- expect(tooltip).toBeInTheDocument()
464
- expect(tooltip).toBeEmptyDOMElement()
465
- })
528
+
529
+ await openByHover(user, screen.getByRole('button'))
530
+
531
+ const tooltip = screen.getByRole('tooltip')
532
+ expect(tooltip).toBeInTheDocument()
533
+ expect(tooltip).toBeEmptyDOMElement()
466
534
  })
467
535
 
468
536
  it('handles null content', async () => {
469
537
  const user = userEvent.setup()
470
538
  render(
471
- <Tooltip content={null}>
539
+ <Tooltip content={null} delayDuration={0}>
472
540
  <button>Hover me</button>
473
541
  </Tooltip>
474
542
  )
475
-
476
- const trigger = screen.getByRole('button')
477
- await user.hover(trigger)
478
-
479
- await waitFor(() => {
480
- const tooltip = screen.getByRole('tooltip')
481
- expect(tooltip).toBeInTheDocument()
482
- })
543
+
544
+ await openByHover(user, screen.getByRole('button'))
545
+
546
+ expect(screen.getByRole('tooltip')).toBeInTheDocument()
483
547
  })
484
548
 
485
549
  it('handles undefined content', async () => {
486
550
  const user = userEvent.setup()
487
551
  render(
488
- <Tooltip content={undefined as React.ReactNode}>
552
+ <Tooltip content={undefined as React.ReactNode} delayDuration={0}>
489
553
  <button>Hover me</button>
490
554
  </Tooltip>
491
555
  )
492
-
493
- const trigger = screen.getByRole('button')
494
- await user.hover(trigger)
495
-
496
- await waitFor(() => {
497
- const tooltip = screen.getByRole('tooltip')
498
- expect(tooltip).toBeInTheDocument()
499
- })
556
+
557
+ await openByHover(user, screen.getByRole('button'))
558
+
559
+ expect(screen.getByRole('tooltip')).toBeInTheDocument()
500
560
  })
501
561
 
502
562
  it('handles single child requirement', () => {
503
- // Tooltip component expects single child due to React.Children.only
563
+ // TooltipTrigger `asChild` ile tek bir çocuk bekler; sarmalayıcı div ile birden fazla düğüm taşınır
504
564
  render(
505
565
  <Tooltip content="Test tooltip">
506
566
  <div>
@@ -509,7 +569,7 @@ describe('Tooltip Components', () => {
509
569
  </div>
510
570
  </Tooltip>
511
571
  )
512
-
572
+
513
573
  // Should render both buttons wrapped in div
514
574
  expect(screen.getByText('Button 1')).toBeInTheDocument()
515
575
  expect(screen.getByText('Button 2')).toBeInTheDocument()
@@ -518,24 +578,23 @@ describe('Tooltip Components', () => {
518
578
  it('passes through HTML attributes', async () => {
519
579
  const user = userEvent.setup()
520
580
  render(
521
- <Tooltip
522
- content="Test tooltip"
523
- data-testid="tooltip-wrapper"
581
+ <Tooltip
582
+ content="Test tooltip"
583
+ delayDuration={0}
584
+ data-testid="tooltip-content"
524
585
  aria-label="Custom tooltip"
525
586
  >
526
587
  <button>Hover me</button>
527
588
  </Tooltip>
528
589
  )
529
-
530
- const trigger = screen.getByRole('button')
531
- await user.hover(trigger)
532
-
533
- await waitFor(() => {
534
- const tooltip = screen.getByRole('tooltip')
535
- expect(tooltip).toBeInTheDocument()
536
- expect(tooltip).toHaveAttribute('data-testid', 'tooltip-wrapper')
537
- expect(tooltip).toHaveAttribute('aria-label', 'Custom tooltip')
538
- })
590
+
591
+ await openByHover(user, screen.getByRole('button'))
592
+
593
+ // Fazladan prop'lar TooltipContent'e (görünür kutuya) iner
594
+ const tooltip = getTooltipContent()
595
+ expect(tooltip).toHaveAttribute('data-testid', 'tooltip-content')
596
+ // `aria-label` Radix tarafından tüketilir: erişilebilir tooltip metnini o belirler
597
+ expect(screen.getByRole('tooltip')).toHaveTextContent('Custom tooltip')
539
598
  })
540
599
 
541
600
  it('handles rapid hover/unhover', async () => {
@@ -545,14 +604,14 @@ describe('Tooltip Components', () => {
545
604
  <button>Hover me</button>
546
605
  </Tooltip>
547
606
  )
548
-
607
+
549
608
  const trigger = screen.getByRole('button')
550
-
609
+
551
610
  // Rapid hover/unhover
552
611
  await user.hover(trigger)
553
612
  await user.unhover(trigger)
554
613
  await user.hover(trigger)
555
-
614
+
556
615
  await waitFor(() => {
557
616
  expect(screen.getByRole('tooltip')).toBeInTheDocument()
558
617
  })
@@ -561,21 +620,85 @@ describe('Tooltip Components', () => {
561
620
  it('handles very long content', async () => {
562
621
  const user = userEvent.setup()
563
622
  const longContent = 'This is a very long tooltip content that should wrap properly and not break the layout. '.repeat(10)
564
-
623
+
565
624
  render(
566
- <Tooltip content={longContent}>
625
+ <Tooltip content={longContent} delayDuration={0}>
567
626
  <button>Hover me</button>
568
627
  </Tooltip>
569
628
  )
570
-
571
- const trigger = screen.getByRole('button')
572
- await user.hover(trigger)
573
-
629
+
630
+ await openByHover(user, screen.getByRole('button'))
631
+
632
+ const tooltip = screen.getByRole('tooltip')
633
+ expect(tooltip).toBeInTheDocument()
634
+ // Check if content contains the expected text (ignoring whitespace differences)
635
+ expect(tooltip.textContent?.trim()).toContain('This is a very long tooltip content')
636
+ })
637
+ })
638
+
639
+ describe('Complex Combinations', () => {
640
+ it('renders with all props', async () => {
641
+ const user = userEvent.setup()
642
+ render(
643
+ <Tooltip
644
+ content="Complex tooltip"
645
+ variant="success"
646
+ size="lg"
647
+ side="bottom"
648
+ align="end"
649
+ delayDuration={50}
650
+ sideOffset={8}
651
+ showArrow
652
+ className="custom-tooltip"
653
+ >
654
+ <button>Complex button</button>
655
+ </Tooltip>
656
+ )
657
+
658
+ await openByHover(user, screen.getByRole('button'))
659
+
660
+ const tooltip = getTooltipContent()
661
+ expect(tooltip).toBeInTheDocument()
662
+ expect(tooltip).toHaveClass('bg-green-500')
663
+ expect(tooltip).toHaveClass('text-base')
664
+ expect(tooltip).toHaveClass('custom-tooltip')
665
+ expect(tooltip).toHaveAttribute('data-side', 'bottom')
666
+ expect(tooltip).toHaveAttribute('data-align', 'end')
667
+ expect(tooltip.querySelector('svg')).not.toBeNull()
668
+ expect(getPopperWrapper()).toHaveStyle({ transform: 'translate(0px, 8px)' })
669
+ })
670
+
671
+ it('works with different trigger elements', async () => {
672
+ const user = userEvent.setup()
673
+ render(
674
+ <div>
675
+ <Tooltip content="Button tooltip" delayDuration={0}>
676
+ <button data-testid="tooltip-button">Button</button>
677
+ </Tooltip>
678
+ <Tooltip content="Link tooltip" delayDuration={0}>
679
+ <a href="#" role="link">Link</a>
680
+ </Tooltip>
681
+ <Tooltip content="Div tooltip" delayDuration={0}>
682
+ <div role="button" tabIndex={0}>Div</div>
683
+ </Tooltip>
684
+ </div>
685
+ )
686
+
687
+ // Test button
688
+ await user.hover(screen.getByTestId('tooltip-button'))
574
689
  await waitFor(() => {
575
- const tooltip = screen.getByRole('tooltip')
576
- expect(tooltip).toBeInTheDocument()
577
- // Check if content contains the expected text (ignoring whitespace differences)
578
- expect(tooltip.textContent?.trim()).toContain('This is a very long tooltip content')
690
+ expect(screen.getByRole('tooltip')).toHaveTextContent('Button tooltip')
691
+ })
692
+
693
+ await movePointerAway(user, screen.getByTestId('tooltip-button'))
694
+ await waitFor(() => {
695
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
696
+ })
697
+
698
+ // Test link
699
+ await user.hover(screen.getByRole('link'))
700
+ await waitFor(() => {
701
+ expect(screen.getByRole('tooltip')).toHaveTextContent('Link tooltip')
579
702
  })
580
703
  })
581
704
  })