@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,177 +1,252 @@
1
1
  import React from 'react'
2
- import { render, screen, fireEvent } from '@testing-library/react'
2
+ import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
3
3
  import userEvent from '@testing-library/user-event'
4
- import {
5
- Toast,
6
- ToastProvider,
4
+ import {
5
+ Toast,
6
+ ToastProvider,
7
+ ToastViewport,
7
8
  ToastTitle,
8
- ToastDescription,
9
- ToastAction,
10
- ToastClose,
9
+ ToastDescription,
10
+ ToastAction,
11
+ ToastClose,
11
12
  useToast,
13
+ toast,
12
14
  Toaster
13
15
  } from '../toast'
14
16
 
17
+ // GERÇEK API NOTLARI (kaynak: ../toast.tsx — `@radix-ui/react-toast` üzerine kurulu)
18
+ //
19
+ // 1) Toast prop tabanlı değil BİLEŞİK (compound) bir API'dir:
20
+ // <Toast><ToastTitle/><ToastDescription/><ToastAction/><ToastClose/></Toast>.
21
+ // `title` / `description` / `action` / `onClose` prop'ları YOKTUR.
22
+ // 2) Radix Toast Root, Provider'ın viewport'u bağlanana kadar HİÇBİR ŞEY render etmez
23
+ // (`if (!context.viewport) return null`) ve içeriğini viewport'a portal'lar.
24
+ // Bu yüzden her render'da `<ToastViewport />` zorunludur.
25
+ // 3) `toastVariants` CVA'sındaki gerçek varyantlar: default | destructive | success |
26
+ // warning | info. (`primary`, `secondary`, `caution` yoktur; success/warning/info
27
+ // yumuşak arka planlar kullanır: bg-green-50 / bg-orange-50 / bg-blue-50.)
28
+ // 4) `ToastAction` Radix tarafında zorunlu `altText` ister (yoksa `altText.trim()` patlar).
29
+ // 5) `toast()` / `useToast()` modül düzeyinde TEK bir store paylaşır; `dismiss` yalnızca
30
+ // `open` bayrağını düşürür (kayıt store'da kalır, kaldırma çok uzun bir gecikmeye bağlıdır).
31
+ // Bu yüzden store testleri kimlik (id) üzerinden yazıldı, mutlak sayı üzerinden değil.
32
+
15
33
  // Mock icons for testing
16
34
  jest.mock('lucide-react', () => ({
17
35
  X: (props: React.ComponentProps<'div'>) => <div data-testid="x-icon" {...props} />,
18
36
  }))
19
37
 
20
- // Test wrapper component for useToast hook
38
+ beforeAll(() => {
39
+ // jsdom Pointer Capture API'sini uygulamaz; Radix Toast'un swipe handler'ı çağırır.
40
+ if (!Element.prototype.hasPointerCapture) {
41
+ Element.prototype.hasPointerCapture = () => false
42
+ Element.prototype.setPointerCapture = () => undefined
43
+ Element.prototype.releasePointerCapture = () => undefined
44
+ }
45
+ })
46
+
47
+ // Test wrapper: Radix, viewport olmadan toast render etmez (bkz. not 2)
21
48
  const TestToastWrapper = ({ children }: { children: React.ReactNode }) => (
22
- <ToastProvider>{children}</ToastProvider>
49
+ <ToastProvider>
50
+ {children}
51
+ <ToastViewport />
52
+ </ToastProvider>
23
53
  )
24
54
 
55
+ // useToast store'unu gözlemleyen yardımcı bileşen
56
+ const StoreProbe = () => {
57
+ const { toasts } = useToast()
58
+ return (
59
+ <div>
60
+ <span data-testid="toast-count">{toasts.length}</span>
61
+ <span data-testid="open-toast-ids">
62
+ {toasts.filter((t) => t.open).map((t) => t.id).join(',')}
63
+ </span>
64
+ <span data-testid="all-toast-ids">{toasts.map((t) => t.id).join(',')}</span>
65
+ </div>
66
+ )
67
+ }
68
+
25
69
  describe('Toast Components', () => {
26
70
  describe('Toast Component', () => {
27
71
  describe('Basic Rendering', () => {
28
72
  it('renders correctly with default props', () => {
29
73
  render(
30
74
  <TestToastWrapper>
31
- <Toast />
75
+ <Toast data-testid="toast">
76
+ <ToastClose />
77
+ </Toast>
32
78
  </TestToastWrapper>
33
79
  )
34
-
35
- const toast = screen.getByRole('button', { name: /kapat/i })
36
- expect(toast).toBeInTheDocument()
80
+
81
+ const toastElement = screen.getByTestId('toast')
82
+ expect(toastElement).toBeInTheDocument()
83
+ // Radix Root bir <li> üretir ve açık durumu data-state ile yayınlar
84
+ expect(toastElement.tagName).toBe('LI')
85
+ expect(toastElement).toHaveAttribute('data-state', 'open')
86
+ // Viewport'a portal'lanır
87
+ expect(toastElement.closest('ol')).toBeInTheDocument()
88
+ expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument()
89
+ })
90
+
91
+ it('renders nothing without a viewport', () => {
92
+ // Radix sözleşmesi: viewport bağlanmadan portal açılmaz
93
+ render(
94
+ <ToastProvider>
95
+ <Toast data-testid="toast">
96
+ <ToastTitle>Görünmez</ToastTitle>
97
+ </Toast>
98
+ </ToastProvider>
99
+ )
100
+
101
+ expect(screen.queryByTestId('toast')).not.toBeInTheDocument()
102
+ expect(screen.queryByText('Görünmez')).not.toBeInTheDocument()
37
103
  })
38
104
 
39
105
  it('renders with title', () => {
40
106
  render(
41
107
  <TestToastWrapper>
42
- <Toast title="Test Title" />
108
+ <Toast>
109
+ <ToastTitle>Test Title</ToastTitle>
110
+ </Toast>
43
111
  </TestToastWrapper>
44
112
  )
45
-
113
+
46
114
  expect(screen.getByText('Test Title')).toBeInTheDocument()
47
115
  })
48
116
 
49
117
  it('renders with description', () => {
50
118
  render(
51
119
  <TestToastWrapper>
52
- <Toast description="Test Description" />
120
+ <Toast>
121
+ <ToastDescription>Test Description</ToastDescription>
122
+ </Toast>
53
123
  </TestToastWrapper>
54
124
  )
55
-
125
+
56
126
  expect(screen.getByText('Test Description')).toBeInTheDocument()
57
127
  })
58
128
 
59
129
  it('applies custom className', () => {
60
130
  const { container } = render(
61
- <ToastProvider>
131
+ <TestToastWrapper>
62
132
  <Toast className="custom-toast" />
63
- </ToastProvider>
133
+ </TestToastWrapper>
64
134
  )
65
-
135
+
66
136
  expect(container.querySelector('.custom-toast')).toBeInTheDocument()
67
137
  })
68
138
 
69
139
  it('has correct displayName', () => {
70
- expect((Toast as any).displayName || 'Toast').toBe('Toast')
140
+ expect(Toast.displayName).toBe('Toast')
71
141
  })
72
142
  })
73
143
 
74
144
  describe('Variants', () => {
75
- it('renders primary variant correctly', () => {
76
- const { container } = render(
77
- <ToastProvider>
78
- <Toast variant="primary" />
79
- </ToastProvider>
80
- )
81
-
82
- expect(container.querySelector('[class*="bg-primary"]')).toBeInTheDocument()
83
- })
84
-
85
- it('renders secondary variant correctly', () => {
86
- const { container } = render(
87
- <ToastProvider>
88
- <Toast variant="secondary" />
89
- </ToastProvider>
145
+ // toastVariants CVA'sındaki gerçek varyantlar ve ürettikleri sınıflar
146
+ const variants = [
147
+ { variant: 'default', classes: ['bg-background', 'text-foreground'] },
148
+ { variant: 'destructive', classes: ['bg-destructive', 'text-destructive-foreground'] },
149
+ { variant: 'success', classes: ['bg-green-50', 'text-green-900', 'border-green-200'] },
150
+ { variant: 'warning', classes: ['bg-orange-50', 'text-orange-900', 'border-orange-200'] },
151
+ { variant: 'info', classes: ['bg-blue-50', 'text-blue-900', 'border-blue-200'] },
152
+ ] as const
153
+
154
+ variants.forEach(({ variant, classes }) => {
155
+ it(`renders ${variant} variant correctly`, () => {
156
+ render(
157
+ <TestToastWrapper>
158
+ <Toast variant={variant} data-testid="toast" />
159
+ </TestToastWrapper>
160
+ )
161
+
162
+ const toastElement = screen.getByTestId('toast')
163
+ classes.forEach((cls) => expect(toastElement).toHaveClass(cls))
164
+ })
165
+ })
166
+
167
+ it('keeps the base layout classes for every variant', () => {
168
+ render(
169
+ <TestToastWrapper>
170
+ <Toast variant="success" data-testid="toast" />
171
+ </TestToastWrapper>
90
172
  )
91
-
92
- expect(container.querySelector('[class*="bg-secondary"]')).toBeInTheDocument()
93
- })
94
173
 
95
- it('renders success variant correctly', () => {
96
- const { container } = render(
97
- <ToastProvider>
98
- <Toast variant="success" />
99
- </ToastProvider>
100
- )
101
-
102
- expect(container.querySelector('[class*="bg-success"]')).toBeInTheDocument()
174
+ const toastElement = screen.getByTestId('toast')
175
+ expect(toastElement).toHaveClass('moonui-theme')
176
+ expect(toastElement).toHaveClass('group')
177
+ expect(toastElement).toHaveClass('rounded-md')
178
+ expect(toastElement).toHaveClass('shadow-sm')
103
179
  })
180
+ })
104
181
 
105
- it('renders caution variant correctly', () => {
106
- const { container } = render(
107
- <ToastProvider>
108
- <Toast variant="caution" />
109
- </ToastProvider>
182
+ describe('Actions', () => {
183
+ it('renders action element', () => {
184
+ render(
185
+ <TestToastWrapper>
186
+ <Toast>
187
+ <ToastAction altText="Try again" data-testid="toast-action">
188
+ Action
189
+ </ToastAction>
190
+ </Toast>
191
+ </TestToastWrapper>
110
192
  )
111
-
112
- expect(container.querySelector('[class*="bg-warning"]')).toBeInTheDocument()
113
- })
114
193
 
115
- it('renders destructive variant correctly', () => {
116
- const { container } = render(
117
- <ToastProvider>
118
- <Toast variant="destructive" />
119
- </ToastProvider>
120
- )
121
-
122
- expect(container.querySelector('[class*="bg-destructive"]')).toBeInTheDocument()
194
+ expect(screen.getByTestId('toast-action')).toBeInTheDocument()
195
+ expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument()
123
196
  })
124
197
 
125
- it('renders info variant correctly', () => {
126
- const { container } = render(
127
- <ToastProvider>
128
- <Toast variant="info" />
129
- </ToastProvider>
130
- )
131
-
132
- expect(container.querySelector('[class*="bg-info"]')).toBeInTheDocument()
133
- })
134
- })
198
+ it('handles action button click', async () => {
199
+ const onClick = jest.fn()
200
+ const user = userEvent.setup()
135
201
 
136
- describe('Actions', () => {
137
- it('renders action element', () => {
138
- const action = <button data-testid="toast-action">Action</button>
139
202
  render(
140
203
  <TestToastWrapper>
141
- <Toast action={action} />
204
+ <Toast>
205
+ <ToastAction altText="Try again" onClick={onClick}>
206
+ Action
207
+ </ToastAction>
208
+ </Toast>
142
209
  </TestToastWrapper>
143
210
  )
144
-
145
- expect(screen.getByTestId('toast-action')).toBeInTheDocument()
211
+
212
+ await user.click(screen.getByRole('button', { name: 'Action' }))
213
+
214
+ expect(onClick).toHaveBeenCalled()
146
215
  })
147
216
 
148
217
  it('handles close button click', async () => {
149
- const onClose = jest.fn()
218
+ const onOpenChange = jest.fn()
150
219
  const user = userEvent.setup()
151
-
220
+
152
221
  render(
153
222
  <TestToastWrapper>
154
- <Toast id="test-toast" onClose={onClose} />
223
+ <Toast data-testid="toast" onOpenChange={onOpenChange}>
224
+ <ToastClose />
225
+ </Toast>
155
226
  </TestToastWrapper>
156
227
  )
157
-
158
- const closeButton = screen.getByRole('button', { name: /kapat/i })
228
+
229
+ const closeButton = screen.getByRole('button', { name: /close/i })
159
230
  await user.click(closeButton)
160
-
161
- expect(onClose).toHaveBeenCalled()
231
+
232
+ // Kapatma Radix üzerinden yürür: açık durum düşer ve toast DOM'dan kalkar
233
+ expect(onOpenChange).toHaveBeenCalledWith(false)
234
+ await waitFor(() => {
235
+ expect(screen.queryByTestId('toast')).not.toBeInTheDocument()
236
+ })
162
237
  })
163
238
  })
164
239
 
165
240
  describe('HTML Attributes', () => {
166
241
  it('passes through HTML attributes', () => {
167
- const { container } = render(
242
+ render(
168
243
  <TestToastWrapper>
169
244
  <Toast data-testid="toast" aria-label="Custom toast" />
170
245
  </TestToastWrapper>
171
246
  )
172
-
173
- const toast = container.querySelector('[data-testid="toast"]')
174
- expect(toast).toHaveAttribute('aria-label', 'Custom toast')
247
+
248
+ const toastElement = screen.getByTestId('toast')
249
+ expect(toastElement).toHaveAttribute('aria-label', 'Custom toast')
175
250
  })
176
251
  })
177
252
  })
@@ -184,18 +259,20 @@ describe('Toast Components', () => {
184
259
  <div data-testid="child">Child Content</div>
185
260
  </ToastProvider>
186
261
  )
187
-
262
+
188
263
  expect(screen.getByTestId('child')).toBeInTheDocument()
189
264
  })
190
265
 
191
266
  it('renders toast container', () => {
192
- const { container } = render(
267
+ render(
193
268
  <ToastProvider>
194
269
  <div>Content</div>
270
+ <ToastViewport />
195
271
  </ToastProvider>
196
272
  )
197
-
198
- const toastContainer = container.querySelector('[aria-label="Bildirimler"]')
273
+
274
+ // Radix viewport'u erişilebilir bir bölge olarak yayınlar (varsayılan etiket: "Notifications (F8)")
275
+ const toastContainer = screen.getByRole('region', { name: /notifications/i })
199
276
  expect(toastContainer).toBeInTheDocument()
200
277
  })
201
278
  })
@@ -203,45 +280,82 @@ describe('Toast Components', () => {
203
280
  describe('Toast Management', () => {
204
281
  it('renders toasts in provider', () => {
205
282
  render(
206
- <ToastProvider>
207
- <Toast title="Test Toast" />
208
- </ToastProvider>
283
+ <TestToastWrapper>
284
+ <Toast>
285
+ <ToastTitle>Test Toast</ToastTitle>
286
+ </Toast>
287
+ </TestToastWrapper>
209
288
  )
210
-
289
+
211
290
  expect(screen.getByText('Test Toast')).toBeInTheDocument()
212
291
  })
213
292
 
214
293
  it('renders multiple toasts', () => {
215
294
  render(
216
- <ToastProvider>
217
- <Toast title="Toast 1" />
218
- <Toast title="Toast 2" />
219
- </ToastProvider>
295
+ <TestToastWrapper>
296
+ <Toast>
297
+ <ToastTitle>Toast 1</ToastTitle>
298
+ </Toast>
299
+ <Toast>
300
+ <ToastTitle>Toast 2</ToastTitle>
301
+ </Toast>
302
+ </TestToastWrapper>
220
303
  )
221
-
304
+
222
305
  expect(screen.getByText('Toast 1')).toBeInTheDocument()
223
306
  expect(screen.getByText('Toast 2')).toBeInTheDocument()
224
307
  })
225
308
  })
226
309
  })
227
310
 
311
+ describe('ToastViewport Component', () => {
312
+ describe('Basic Rendering', () => {
313
+ it('renders an ordered list with the positioning classes', () => {
314
+ const { container } = render(
315
+ <ToastProvider>
316
+ <ToastViewport />
317
+ </ToastProvider>
318
+ )
319
+
320
+ const viewport = container.querySelector('ol') as HTMLElement
321
+ expect(viewport).toBeInTheDocument()
322
+ expect(viewport).toHaveClass('fixed')
323
+ expect(viewport).toHaveClass('z-[100]')
324
+ })
325
+
326
+ it('applies custom className', () => {
327
+ const { container } = render(
328
+ <ToastProvider>
329
+ <ToastViewport className="custom-viewport" />
330
+ </ToastProvider>
331
+ )
332
+
333
+ expect(container.querySelector('.custom-viewport')).toBeInTheDocument()
334
+ })
335
+
336
+ it('has correct displayName', () => {
337
+ expect(ToastViewport.displayName).toBe('ToastViewport')
338
+ })
339
+ })
340
+ })
341
+
228
342
  describe('ToastTitle Component', () => {
229
343
  describe('Basic Rendering', () => {
230
344
  it('renders correctly with default props', () => {
231
345
  render(<ToastTitle>Title Content</ToastTitle>)
232
-
346
+
233
347
  expect(screen.getByText('Title Content')).toBeInTheDocument()
234
348
  })
235
349
 
236
350
  it('applies custom className', () => {
237
351
  render(<ToastTitle className="custom-title">Title</ToastTitle>)
238
-
352
+
239
353
  expect(screen.getByText('Title')).toHaveClass('custom-title')
240
354
  })
241
355
 
242
356
  it('passes through HTML attributes', () => {
243
357
  render(<ToastTitle data-testid="toast-title">Title</ToastTitle>)
244
-
358
+
245
359
  expect(screen.getByTestId('toast-title')).toBeInTheDocument()
246
360
  })
247
361
  })
@@ -251,19 +365,19 @@ describe('Toast Components', () => {
251
365
  describe('Basic Rendering', () => {
252
366
  it('renders correctly with default props', () => {
253
367
  render(<ToastDescription>Description Content</ToastDescription>)
254
-
368
+
255
369
  expect(screen.getByText('Description Content')).toBeInTheDocument()
256
370
  })
257
371
 
258
372
  it('applies custom className', () => {
259
373
  render(<ToastDescription className="custom-desc">Description</ToastDescription>)
260
-
374
+
261
375
  expect(screen.getByText('Description')).toHaveClass('custom-desc')
262
376
  })
263
377
 
264
378
  it('passes through HTML attributes', () => {
265
379
  render(<ToastDescription data-testid="toast-desc">Description</ToastDescription>)
266
-
380
+
267
381
  expect(screen.getByTestId('toast-desc')).toBeInTheDocument()
268
382
  })
269
383
  })
@@ -272,29 +386,51 @@ describe('Toast Components', () => {
272
386
  describe('ToastAction Component', () => {
273
387
  describe('Basic Rendering', () => {
274
388
  it('renders correctly with default props', () => {
275
- render(<ToastAction>Action</ToastAction>)
276
-
389
+ render(<ToastAction altText="Retry the failed action">Action</ToastAction>)
390
+
277
391
  expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument()
278
392
  })
279
393
 
394
+ it('exposes altText to the screen reader announcement', () => {
395
+ render(<ToastAction altText="Retry the failed action">Action</ToastAction>)
396
+
397
+ // Radix, aksiyonun sesli duyuruda nasıl okunacağını bu veri özniteliğinden alır
398
+ expect(screen.getByRole('button')).toHaveAttribute(
399
+ 'data-radix-toast-announce-alt',
400
+ 'Retry the failed action'
401
+ )
402
+ })
403
+
280
404
  it('applies custom className', () => {
281
- render(<ToastAction className="custom-action">Action</ToastAction>)
282
-
405
+ render(
406
+ <ToastAction altText="Retry" className="custom-action">
407
+ Action
408
+ </ToastAction>
409
+ )
410
+
283
411
  expect(screen.getByRole('button')).toHaveClass('custom-action')
284
412
  })
285
413
 
286
414
  it('handles click events', async () => {
287
415
  const onClick = jest.fn()
288
-
289
- render(<ToastAction onClick={onClick}>Action</ToastAction>)
290
-
416
+
417
+ render(
418
+ <ToastAction altText="Retry" onClick={onClick}>
419
+ Action
420
+ </ToastAction>
421
+ )
422
+
291
423
  fireEvent.click(screen.getByRole('button'))
292
424
  expect(onClick).toHaveBeenCalled()
293
425
  })
294
426
 
295
427
  it('passes through HTML attributes', () => {
296
- render(<ToastAction data-testid="toast-action">Action</ToastAction>)
297
-
428
+ render(
429
+ <ToastAction altText="Retry" data-testid="toast-action">
430
+ Action
431
+ </ToastAction>
432
+ )
433
+
298
434
  expect(screen.getByTestId('toast-action')).toBeInTheDocument()
299
435
  })
300
436
  })
@@ -304,58 +440,205 @@ describe('Toast Components', () => {
304
440
  describe('Basic Rendering', () => {
305
441
  it('renders correctly with default props', () => {
306
442
  render(<ToastClose />)
307
-
308
- expect(screen.getByRole('button', { name: /kapat/i })).toBeInTheDocument()
443
+
444
+ // İkon-only düğmenin erişilebilir bir ismi olmalı (WCAG 4.1.2)
445
+ expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument()
309
446
  expect(screen.getByTestId('x-icon')).toBeInTheDocument()
310
447
  })
311
448
 
312
449
  it('applies custom className', () => {
313
450
  render(<ToastClose className="custom-close" />)
314
-
451
+
315
452
  expect(screen.getByRole('button')).toHaveClass('custom-close')
316
453
  })
317
454
 
318
455
  it('handles click events', async () => {
319
456
  const onClick = jest.fn()
320
-
457
+
321
458
  render(<ToastClose onClick={onClick} />)
322
-
459
+
323
460
  fireEvent.click(screen.getByRole('button'))
324
461
  expect(onClick).toHaveBeenCalled()
325
462
  })
326
463
 
327
464
  it('passes through HTML attributes', () => {
328
465
  render(<ToastClose data-testid="toast-close" />)
329
-
466
+
330
467
  expect(screen.getByTestId('toast-close')).toBeInTheDocument()
468
+ expect(screen.getByTestId('toast-close')).toHaveAttribute('toast-close', '')
331
469
  })
332
470
  })
333
471
  })
334
472
 
335
473
  describe('useToast Hook', () => {
336
474
  describe('Context Usage', () => {
475
+ // NOT: store modül düzeyinde paylaşıldığından bu blok, store'a yazan
476
+ // testlerden ÖNCE gelir (boş başlangıç durumunu ölçer).
337
477
  it('works correctly within ToastProvider', () => {
338
478
  const TestComponent = () => {
339
479
  const { toasts } = useToast()
340
480
  return <div data-testid="toast-count">{toasts.length}</div>
341
481
  }
342
-
482
+
343
483
  render(
344
484
  <ToastProvider>
345
485
  <TestComponent />
486
+ <ToastViewport />
346
487
  </ToastProvider>
347
488
  )
348
-
489
+
349
490
  expect(screen.getByTestId('toast-count')).toHaveTextContent('0')
350
491
  })
351
492
  })
493
+
494
+ describe('Store Behaviour', () => {
495
+ it('adds a toast to the store and renders it through Toaster', async () => {
496
+ render(
497
+ <div>
498
+ <StoreProbe />
499
+ <Toaster />
500
+ </div>
501
+ )
502
+
503
+ let created: ReturnType<typeof toast>
504
+ act(() => {
505
+ created = toast({ title: 'Store Toast', description: 'Store description' })
506
+ })
507
+
508
+ await waitFor(() => {
509
+ expect(screen.getByTestId('open-toast-ids')).toHaveTextContent(created!.id)
510
+ })
511
+ expect(screen.getByText('Store Toast')).toBeInTheDocument()
512
+ expect(screen.getByText('Store description')).toBeInTheDocument()
513
+ })
514
+
515
+ it('returns id, dismiss and update handles', () => {
516
+ render(<StoreProbe />)
517
+
518
+ let created: ReturnType<typeof toast>
519
+ act(() => {
520
+ created = toast({ title: 'Handle Toast' })
521
+ })
522
+
523
+ expect(typeof created!.id).toBe('string')
524
+ expect(created!.id.length).toBeGreaterThan(0)
525
+ expect(typeof created!.dismiss).toBe('function')
526
+ expect(typeof created!.update).toBe('function')
527
+ expect(screen.getByTestId('all-toast-ids')).toHaveTextContent(created!.id)
528
+ })
529
+
530
+ it('dismiss() hides the toast but keeps it in the store', async () => {
531
+ render(
532
+ <div>
533
+ <StoreProbe />
534
+ <Toaster />
535
+ </div>
536
+ )
537
+
538
+ let created: ReturnType<typeof toast>
539
+ act(() => {
540
+ created = toast({ title: 'Dismiss Toast' })
541
+ })
542
+
543
+ await waitFor(() => {
544
+ expect(screen.getByText('Dismiss Toast')).toBeInTheDocument()
545
+ })
546
+
547
+ act(() => {
548
+ created!.dismiss()
549
+ })
550
+
551
+ await waitFor(() => {
552
+ expect(screen.queryByText('Dismiss Toast')).not.toBeInTheDocument()
553
+ })
554
+ // open listesinden düşer ama kayıt store'da kalır (kaldırma gecikmeye bağlıdır)
555
+ expect(screen.getByTestId('open-toast-ids')).not.toHaveTextContent(created!.id)
556
+ expect(screen.getByTestId('all-toast-ids')).toHaveTextContent(created!.id)
557
+ })
558
+
559
+ it('update() changes the rendered toast content', async () => {
560
+ render(
561
+ <div>
562
+ <StoreProbe />
563
+ <Toaster />
564
+ </div>
565
+ )
566
+
567
+ let created: ReturnType<typeof toast>
568
+ act(() => {
569
+ created = toast({ title: 'Before Update' })
570
+ })
571
+
572
+ await waitFor(() => {
573
+ expect(screen.getByText('Before Update')).toBeInTheDocument()
574
+ })
575
+
576
+ act(() => {
577
+ created!.update({ id: created!.id, title: 'After Update' })
578
+ })
579
+
580
+ await waitFor(() => {
581
+ expect(screen.getByText('After Update')).toBeInTheDocument()
582
+ })
583
+ expect(screen.queryByText('Before Update')).not.toBeInTheDocument()
584
+ })
585
+
586
+ it('dismisses every toast when dismiss() is called without an id', async () => {
587
+ const DismissAll = () => {
588
+ const { dismiss } = useToast()
589
+ return <button onClick={() => dismiss()}>dismiss all</button>
590
+ }
591
+
592
+ render(
593
+ <div>
594
+ <StoreProbe />
595
+ <DismissAll />
596
+ <Toaster />
597
+ </div>
598
+ )
599
+
600
+ act(() => {
601
+ toast({ title: 'Bulk One' })
602
+ toast({ title: 'Bulk Two' })
603
+ })
604
+
605
+ await waitFor(() => {
606
+ expect(screen.getByText('Bulk One')).toBeInTheDocument()
607
+ })
608
+
609
+ fireEvent.click(screen.getByRole('button', { name: 'dismiss all' }))
610
+
611
+ await waitFor(() => {
612
+ expect(screen.getByTestId('open-toast-ids')).toHaveTextContent('')
613
+ })
614
+ expect(screen.queryByText('Bulk One')).not.toBeInTheDocument()
615
+ expect(screen.queryByText('Bulk Two')).not.toBeInTheDocument()
616
+ })
617
+
618
+ // NOT: store paylaşıldığı için sınır testi en sonda durur (5'e kırpma
619
+ // önceki testlerden kalan kayıtları da eler).
620
+ it('caps the store at the toast limit', async () => {
621
+ render(<StoreProbe />)
622
+
623
+ act(() => {
624
+ for (let i = 1; i <= 6; i++) {
625
+ toast({ title: `Limit ${i}` })
626
+ }
627
+ })
628
+
629
+ await waitFor(() => {
630
+ expect(screen.getByTestId('toast-count')).toHaveTextContent('5')
631
+ })
632
+ })
633
+ })
352
634
  })
353
635
 
354
636
  describe('Toaster Component', () => {
355
637
  it('renders Toaster component', () => {
356
- render(<Toaster><div>Test content</div></Toaster>)
357
- // Toaster should render without errors
358
- expect(document.body).toBeInTheDocument()
638
+ render(<Toaster />)
639
+
640
+ // Toaster kendi provider + viewport'unu kurar
641
+ expect(screen.getByRole('region', { name: /notifications/i })).toBeInTheDocument()
359
642
  })
360
643
  })
361
644
 
@@ -363,30 +646,38 @@ describe('Toast Components', () => {
363
646
  it('renders complete toast with all components', () => {
364
647
  render(
365
648
  <TestToastWrapper>
366
- <Toast
367
- title="Complete Toast"
368
- description="This is a complete toast"
369
- variant="success"
370
- action={<ToastAction>Retry</ToastAction>}
371
- />
649
+ <Toast variant="success">
650
+ <div className="grid gap-1">
651
+ <ToastTitle>Complete Toast</ToastTitle>
652
+ <ToastDescription>This is a complete toast</ToastDescription>
653
+ </div>
654
+ <ToastAction altText="Retry the failed action">Retry</ToastAction>
655
+ <ToastClose />
656
+ </Toast>
372
657
  </TestToastWrapper>
373
658
  )
374
-
659
+
375
660
  expect(screen.getByText('Complete Toast')).toBeInTheDocument()
376
661
  expect(screen.getByText('This is a complete toast')).toBeInTheDocument()
377
662
  expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument()
378
- expect(screen.getByRole('button', { name: /kapat/i })).toBeInTheDocument()
663
+ expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument()
379
664
  })
380
665
 
381
666
  it('handles multiple toasts', () => {
382
667
  render(
383
668
  <TestToastWrapper>
384
- <Toast title="Toast 1" />
385
- <Toast title="Toast 2" />
386
- <Toast title="Toast 3" />
669
+ <Toast>
670
+ <ToastTitle>Toast 1</ToastTitle>
671
+ </Toast>
672
+ <Toast>
673
+ <ToastTitle>Toast 2</ToastTitle>
674
+ </Toast>
675
+ <Toast>
676
+ <ToastTitle>Toast 3</ToastTitle>
677
+ </Toast>
387
678
  </TestToastWrapper>
388
679
  )
389
-
680
+
390
681
  expect(screen.getByText('Toast 1')).toBeInTheDocument()
391
682
  expect(screen.getByText('Toast 2')).toBeInTheDocument()
392
683
  expect(screen.getByText('Toast 3')).toBeInTheDocument()
@@ -394,46 +685,57 @@ describe('Toast Components', () => {
394
685
  })
395
686
 
396
687
  describe('Edge Cases', () => {
397
- it('handles toast without id', () => {
688
+ it('handles a toast without any children', () => {
398
689
  render(
399
690
  <TestToastWrapper>
400
- <Toast title="No ID Toast" />
691
+ <Toast data-testid="toast" />
401
692
  </TestToastWrapper>
402
693
  )
403
-
404
- expect(screen.getByText('No ID Toast')).toBeInTheDocument()
694
+
695
+ expect(screen.getByTestId('toast')).toBeInTheDocument()
696
+ expect(screen.getByTestId('toast')).toBeEmptyDOMElement()
405
697
  })
406
698
 
407
699
  it('handles empty toast content', () => {
408
700
  render(
409
701
  <TestToastWrapper>
410
- <Toast />
702
+ <Toast>
703
+ <ToastClose />
704
+ </Toast>
411
705
  </TestToastWrapper>
412
706
  )
413
-
414
- expect(screen.getByRole('button', { name: /kapat/i })).toBeInTheDocument()
707
+
708
+ expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument()
415
709
  })
416
710
 
417
- it('handles null/undefined props', () => {
711
+ it('handles null/undefined children', () => {
418
712
  render(
419
713
  <TestToastWrapper>
420
- <Toast title={null} description={undefined} />
714
+ <Toast>
715
+ {null}
716
+ {undefined}
717
+ <ToastClose />
718
+ </Toast>
421
719
  </TestToastWrapper>
422
720
  )
423
-
424
- expect(screen.getByRole('button', { name: /kapat/i })).toBeInTheDocument()
721
+
722
+ expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument()
425
723
  })
426
724
 
427
725
  it('handles complex nested content', () => {
428
726
  render(
429
727
  <TestToastWrapper>
430
- <Toast
431
- title={<span>Complex <strong>Title</strong></span>}
432
- description={<div>Complex <em>Description</em></div>}
433
- />
728
+ <Toast>
729
+ <ToastTitle>
730
+ <span>Complex <strong>Title</strong></span>
731
+ </ToastTitle>
732
+ <ToastDescription>
733
+ <div>Complex <em>Description</em></div>
734
+ </ToastDescription>
735
+ </Toast>
434
736
  </TestToastWrapper>
435
737
  )
436
-
738
+
437
739
  expect(screen.getAllByText('Complex')).toHaveLength(2)
438
740
  expect(screen.getByText('Title')).toBeInTheDocument()
439
741
  expect(screen.getByText('Description')).toBeInTheDocument()
@@ -444,11 +746,11 @@ describe('Toast Components', () => {
444
746
  <div>
445
747
  <ToastTitle data-testid="title" aria-label="Title">Title</ToastTitle>
446
748
  <ToastDescription data-testid="desc" aria-label="Description">Description</ToastDescription>
447
- <ToastAction data-testid="action" aria-label="Action">Action</ToastAction>
749
+ <ToastAction altText="Retry" data-testid="action" aria-label="Action">Action</ToastAction>
448
750
  <ToastClose data-testid="close" aria-label="Close" />
449
751
  </div>
450
752
  )
451
-
753
+
452
754
  expect(screen.getByTestId('title')).toHaveAttribute('aria-label', 'Title')
453
755
  expect(screen.getByTestId('desc')).toHaveAttribute('aria-label', 'Description')
454
756
  expect(screen.getByTestId('action')).toHaveAttribute('aria-label', 'Action')