@moontra/moonui 5.0.0 → 5.1.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.d.mts +15 -0
- package/dist/hooks/index.d.ts +15 -0
- package/dist/hooks/index.global.js +38 -0
- package/dist/hooks/index.global.js.map +1 -0
- package/dist/hooks/index.js +128 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/index.mjs +106 -0
- package/dist/hooks/index.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.global.js +9 -9
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +17 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +17 -11
- package/dist/index.mjs.map +1 -1
- package/dist/lib/theme.global.js.map +1 -1
- package/package.json +8 -3
- package/src/__tests__/hooks-exports-contract.test.ts +22 -0
- package/src/__tests__/use-intersection-observer.test.tsx +18 -4
- package/src/__tests__/use-local-storage.test.tsx +34 -20
- package/src/components/ui/__tests__/avatar.test.tsx +5 -7
- package/src/components/ui/__tests__/badge.test.tsx +19 -12
- package/src/components/ui/__tests__/breadcrumb.test.tsx +4 -7
- package/src/components/ui/__tests__/button.test.tsx +6 -3
- package/src/components/ui/__tests__/card.test.tsx +4 -1
- package/src/components/ui/__tests__/checkbox.test.tsx +4 -1
- package/src/components/ui/__tests__/command.test.tsx +268 -124
- package/src/components/ui/__tests__/data-table-basic.test.tsx +271 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +184 -197
- package/src/components/ui/__tests__/progress.test.tsx +0 -1
- package/src/components/ui/__tests__/table.test.tsx +8 -2
- package/src/components/ui/__tests__/tabs.test.tsx +5 -3
- package/src/components/ui/__tests__/toast.test.tsx +461 -159
- package/src/components/ui/__tests__/tooltip.test.tsx +387 -264
- package/src/components/ui/command.tsx +11 -7
- package/src/components/ui/date-picker.tsx +12 -6
- package/src/components/ui/toast.tsx +3 -0
- package/src/hooks/index.ts +24 -0
- package/src/use-intersection-observer.tsx +10 -1
- package/src/use-local-storage.tsx +35 -1
- package/src/components/ui/__tests__/data-table.test.tsx +0 -256
- 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
|
-
|
|
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>
|
|
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
|
|
36
|
-
expect(
|
|
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
|
|
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
|
|
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
|
-
<
|
|
131
|
+
<TestToastWrapper>
|
|
62
132
|
<Toast className="custom-toast" />
|
|
63
|
-
</
|
|
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(
|
|
140
|
+
expect(Toast.displayName).toBe('Toast')
|
|
71
141
|
})
|
|
72
142
|
})
|
|
73
143
|
|
|
74
144
|
describe('Variants', () => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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('
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
|
204
|
+
<Toast>
|
|
205
|
+
<ToastAction altText="Try again" onClick={onClick}>
|
|
206
|
+
Action
|
|
207
|
+
</ToastAction>
|
|
208
|
+
</Toast>
|
|
142
209
|
</TestToastWrapper>
|
|
143
210
|
)
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
218
|
+
const onOpenChange = jest.fn()
|
|
150
219
|
const user = userEvent.setup()
|
|
151
|
-
|
|
220
|
+
|
|
152
221
|
render(
|
|
153
222
|
<TestToastWrapper>
|
|
154
|
-
<Toast
|
|
223
|
+
<Toast data-testid="toast" onOpenChange={onOpenChange}>
|
|
224
|
+
<ToastClose />
|
|
225
|
+
</Toast>
|
|
155
226
|
</TestToastWrapper>
|
|
156
227
|
)
|
|
157
|
-
|
|
158
|
-
const closeButton = screen.getByRole('button', { name: /
|
|
228
|
+
|
|
229
|
+
const closeButton = screen.getByRole('button', { name: /close/i })
|
|
159
230
|
await user.click(closeButton)
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
242
|
+
render(
|
|
168
243
|
<TestToastWrapper>
|
|
169
244
|
<Toast data-testid="toast" aria-label="Custom toast" />
|
|
170
245
|
</TestToastWrapper>
|
|
171
246
|
)
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
expect(
|
|
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
|
-
|
|
267
|
+
render(
|
|
193
268
|
<ToastProvider>
|
|
194
269
|
<div>Content</div>
|
|
270
|
+
<ToastViewport />
|
|
195
271
|
</ToastProvider>
|
|
196
272
|
)
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
<
|
|
207
|
-
<Toast
|
|
208
|
-
|
|
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
|
-
<
|
|
217
|
-
<Toast
|
|
218
|
-
|
|
219
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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: /
|
|
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
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
688
|
+
it('handles a toast without any children', () => {
|
|
398
689
|
render(
|
|
399
690
|
<TestToastWrapper>
|
|
400
|
-
<Toast
|
|
691
|
+
<Toast data-testid="toast" />
|
|
401
692
|
</TestToastWrapper>
|
|
402
693
|
)
|
|
403
|
-
|
|
404
|
-
expect(screen.
|
|
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: /
|
|
707
|
+
|
|
708
|
+
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument()
|
|
415
709
|
})
|
|
416
710
|
|
|
417
|
-
it('handles null/undefined
|
|
711
|
+
it('handles null/undefined children', () => {
|
|
418
712
|
render(
|
|
419
713
|
<TestToastWrapper>
|
|
420
|
-
<Toast
|
|
714
|
+
<Toast>
|
|
715
|
+
{null}
|
|
716
|
+
{undefined}
|
|
717
|
+
<ToastClose />
|
|
718
|
+
</Toast>
|
|
421
719
|
</TestToastWrapper>
|
|
422
720
|
)
|
|
423
|
-
|
|
424
|
-
expect(screen.getByRole('button', { name: /
|
|
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
|
-
|
|
432
|
-
|
|
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')
|