@startsimpli/ui 0.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/README.md +537 -0
- package/package.json +80 -0
- package/src/components/index.ts +50 -0
- package/src/components/navigation/sidebar.tsx +178 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +70 -0
- package/src/components/ui/card.tsx +68 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +12 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/index.ts +24 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +31 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +24 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/components/unified-table/UnifiedTable.tsx +553 -0
- package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
- package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
- package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
- package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
- package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
- package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
- package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
- package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
- package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
- package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
- package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
- package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
- package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
- package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
- package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
- package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
- package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
- package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
- package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
- package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
- package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
- package/src/components/unified-table/components/MobileView/README.md +411 -0
- package/src/components/unified-table/components/MobileView/index.tsx +77 -0
- package/src/components/unified-table/components/MobileView/types.ts +77 -0
- package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
- package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
- package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
- package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
- package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
- package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
- package/src/components/unified-table/hooks/index.ts +21 -0
- package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
- package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
- package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
- package/src/components/unified-table/hooks/useFilters.ts +53 -0
- package/src/components/unified-table/hooks/usePagination.ts +120 -0
- package/src/components/unified-table/hooks/useResponsive.ts +50 -0
- package/src/components/unified-table/hooks/useSelection.ts +152 -0
- package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
- package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
- package/src/components/unified-table/hooks/useTableState.ts +103 -0
- package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
- package/src/components/unified-table/hooks/useTableURL.ts +301 -0
- package/src/components/unified-table/index.ts +16 -0
- package/src/components/unified-table/types.ts +393 -0
- package/src/components/unified-table/utils/export.ts +236 -0
- package/src/components/unified-table/utils/index.ts +4 -0
- package/src/components/unified-table/utils/renderers.ts +105 -0
- package/src/components/unified-table/utils/themes.ts +87 -0
- package/src/components/unified-table/utils/validation.ts +122 -0
- package/src/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/theme/contract.ts +46 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/tailwind.config.js +70 -0
- package/src/theme/tailwind.preset.ts +93 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/index.ts +91 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { ExportButton } from '../../components/Toolbar/ExportButton'
|
|
4
|
+
import { ColumnConfig } from '../../types'
|
|
5
|
+
import * as exportUtils from '../../utils/export'
|
|
6
|
+
|
|
7
|
+
jest.mock('../../utils/export', () => ({
|
|
8
|
+
exportToCSV: jest.fn(),
|
|
9
|
+
exportToExcel: jest.fn(),
|
|
10
|
+
generateExportFilename: jest.fn(() => 'mocked-timestamp'),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
interface TestData {
|
|
14
|
+
id: string
|
|
15
|
+
name: string
|
|
16
|
+
email: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('ExportButton', () => {
|
|
20
|
+
const mockData: TestData[] = [
|
|
21
|
+
{ id: '1', name: 'John Doe', email: 'john@example.com' },
|
|
22
|
+
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
|
|
23
|
+
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const mockFilteredData: TestData[] = [
|
|
27
|
+
{ id: '1', name: 'John Doe', email: 'john@example.com' },
|
|
28
|
+
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const mockSelectedData: TestData[] = [{ id: '1', name: 'John Doe', email: 'john@example.com' }]
|
|
32
|
+
|
|
33
|
+
const mockColumns: ColumnConfig<TestData>[] = [
|
|
34
|
+
{ id: 'id', header: 'ID', accessorKey: 'id' },
|
|
35
|
+
{ id: 'name', header: 'Name', accessorKey: 'name' },
|
|
36
|
+
{ id: 'email', header: 'Email', accessorKey: 'email' },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
jest.clearAllMocks()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('Rendering', () => {
|
|
44
|
+
it('should render export button', () => {
|
|
45
|
+
render(<ExportButton data={mockData} columns={mockColumns} />)
|
|
46
|
+
|
|
47
|
+
expect(screen.getByRole('button', { name: /export/i })).toBeInTheDocument()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should be disabled when no data', () => {
|
|
51
|
+
render(<ExportButton data={[]} columns={mockColumns} />)
|
|
52
|
+
|
|
53
|
+
expect(screen.getByRole('button', { name: /export/i })).toBeDisabled()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should be disabled when disabled prop is true', () => {
|
|
57
|
+
render(<ExportButton data={mockData} columns={mockColumns} disabled={true} />)
|
|
58
|
+
|
|
59
|
+
expect(screen.getByRole('button', { name: /export/i })).toBeDisabled()
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('Dropdown Menu', () => {
|
|
64
|
+
it('should show dropdown menu when clicked', async () => {
|
|
65
|
+
const user = userEvent.setup()
|
|
66
|
+
render(<ExportButton data={mockData} columns={mockColumns} />)
|
|
67
|
+
|
|
68
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
69
|
+
await user.click(button)
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(screen.getByText('CSV Format')).toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
expect(screen.getByText('Excel Format')).toBeInTheDocument()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should show all export options with correct row counts', async () => {
|
|
78
|
+
const user = userEvent.setup()
|
|
79
|
+
render(<ExportButton data={mockData} columns={mockColumns} />)
|
|
80
|
+
|
|
81
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
82
|
+
await user.click(button)
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
const allOptions = screen.getAllByText(/Export All.*3 rows/i)
|
|
86
|
+
// Should have 2 options: CSV and Excel
|
|
87
|
+
expect(allOptions).toHaveLength(2)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should show filtered data option when available', async () => {
|
|
92
|
+
const user = userEvent.setup()
|
|
93
|
+
render(
|
|
94
|
+
<ExportButton
|
|
95
|
+
data={mockData}
|
|
96
|
+
filteredData={mockFilteredData}
|
|
97
|
+
columns={mockColumns}
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
102
|
+
await user.click(button)
|
|
103
|
+
|
|
104
|
+
await waitFor(() => {
|
|
105
|
+
const filteredOptions = screen.getAllByText(/Export Filtered.*2 rows/i)
|
|
106
|
+
// Should have 2 options: CSV and Excel
|
|
107
|
+
expect(filteredOptions).toHaveLength(2)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should show selected data option when available', async () => {
|
|
112
|
+
const user = userEvent.setup()
|
|
113
|
+
render(
|
|
114
|
+
<ExportButton
|
|
115
|
+
data={mockData}
|
|
116
|
+
selectedData={mockSelectedData}
|
|
117
|
+
columns={mockColumns}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
122
|
+
await user.click(button)
|
|
123
|
+
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
const selectedOptions = screen.getAllByText(/Export Selected.*1 row/i)
|
|
126
|
+
// Should have 2 options: CSV and Excel
|
|
127
|
+
expect(selectedOptions).toHaveLength(2)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should not show filtered option when filtered data equals total data', async () => {
|
|
132
|
+
const user = userEvent.setup()
|
|
133
|
+
render(
|
|
134
|
+
<ExportButton data={mockData} filteredData={mockData} columns={mockColumns} />
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
138
|
+
await user.click(button)
|
|
139
|
+
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(screen.getByText('CSV Format')).toBeInTheDocument()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const filteredOptions = screen.queryAllByText(/Export Filtered/i)
|
|
145
|
+
expect(filteredOptions).toHaveLength(0)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should not show selected option when no data selected', async () => {
|
|
149
|
+
const user = userEvent.setup()
|
|
150
|
+
render(<ExportButton data={mockData} selectedData={[]} columns={mockColumns} />)
|
|
151
|
+
|
|
152
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
153
|
+
await user.click(button)
|
|
154
|
+
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(screen.getByText('CSV Format')).toBeInTheDocument()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const selectedOptions = screen.queryAllByText(/Export Selected/i)
|
|
160
|
+
expect(selectedOptions).toHaveLength(0)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('CSV Export', () => {
|
|
165
|
+
it('should export all data to CSV', async () => {
|
|
166
|
+
const user = userEvent.setup()
|
|
167
|
+
render(<ExportButton data={mockData} columns={mockColumns} />)
|
|
168
|
+
|
|
169
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
170
|
+
await user.click(button)
|
|
171
|
+
|
|
172
|
+
const csvAllOptions = await screen.findAllByText(/Export All.*3 rows/i)
|
|
173
|
+
await user.click(csvAllOptions[0]) // First one is CSV
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(exportUtils.exportToCSV).toHaveBeenCalledWith(
|
|
177
|
+
mockData,
|
|
178
|
+
mockColumns,
|
|
179
|
+
expect.stringContaining('export_all')
|
|
180
|
+
)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should export filtered data to CSV', async () => {
|
|
185
|
+
const user = userEvent.setup()
|
|
186
|
+
render(
|
|
187
|
+
<ExportButton
|
|
188
|
+
data={mockData}
|
|
189
|
+
filteredData={mockFilteredData}
|
|
190
|
+
columns={mockColumns}
|
|
191
|
+
/>
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
195
|
+
await user.click(button)
|
|
196
|
+
|
|
197
|
+
const csvFilteredOptions = await screen.findAllByText(/Export Filtered.*2 rows/i)
|
|
198
|
+
await user.click(csvFilteredOptions[0]) // First one is CSV
|
|
199
|
+
|
|
200
|
+
await waitFor(() => {
|
|
201
|
+
expect(exportUtils.exportToCSV).toHaveBeenCalledWith(
|
|
202
|
+
mockFilteredData,
|
|
203
|
+
mockColumns,
|
|
204
|
+
expect.stringContaining('export_filtered')
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should export selected data to CSV', async () => {
|
|
210
|
+
const user = userEvent.setup()
|
|
211
|
+
render(
|
|
212
|
+
<ExportButton
|
|
213
|
+
data={mockData}
|
|
214
|
+
selectedData={mockSelectedData}
|
|
215
|
+
columns={mockColumns}
|
|
216
|
+
/>
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
220
|
+
await user.click(button)
|
|
221
|
+
|
|
222
|
+
const csvSelectedOptions = await screen.findAllByText(/Export Selected.*1 row/i)
|
|
223
|
+
await user.click(csvSelectedOptions[0]) // First one is CSV
|
|
224
|
+
|
|
225
|
+
await waitFor(() => {
|
|
226
|
+
expect(exportUtils.exportToCSV).toHaveBeenCalledWith(
|
|
227
|
+
mockSelectedData,
|
|
228
|
+
mockColumns,
|
|
229
|
+
expect.stringContaining('export_selected')
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('should use custom base filename', async () => {
|
|
235
|
+
const user = userEvent.setup()
|
|
236
|
+
render(
|
|
237
|
+
<ExportButton
|
|
238
|
+
data={mockData}
|
|
239
|
+
columns={mockColumns}
|
|
240
|
+
baseFilename="custom-export"
|
|
241
|
+
/>
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
245
|
+
await user.click(button)
|
|
246
|
+
|
|
247
|
+
const csvAllOptions = await screen.findAllByText(/Export All.*3 rows/i)
|
|
248
|
+
await user.click(csvAllOptions[0]) // First one is CSV
|
|
249
|
+
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(exportUtils.exportToCSV).toHaveBeenCalledWith(
|
|
252
|
+
mockData,
|
|
253
|
+
mockColumns,
|
|
254
|
+
expect.stringContaining('custom-export')
|
|
255
|
+
)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe('Excel Export', () => {
|
|
261
|
+
it('should export all data to Excel', async () => {
|
|
262
|
+
const user = userEvent.setup()
|
|
263
|
+
render(<ExportButton data={mockData} columns={mockColumns} />)
|
|
264
|
+
|
|
265
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
266
|
+
await user.click(button)
|
|
267
|
+
|
|
268
|
+
const excelAllOptions = await screen.findAllByText(/Export All.*3 rows/i)
|
|
269
|
+
await user.click(excelAllOptions[1])
|
|
270
|
+
|
|
271
|
+
await waitFor(() => {
|
|
272
|
+
expect(exportUtils.exportToExcel).toHaveBeenCalledWith(
|
|
273
|
+
mockData,
|
|
274
|
+
mockColumns,
|
|
275
|
+
expect.stringContaining('export_all')
|
|
276
|
+
)
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should export filtered data to Excel', async () => {
|
|
281
|
+
const user = userEvent.setup()
|
|
282
|
+
render(
|
|
283
|
+
<ExportButton
|
|
284
|
+
data={mockData}
|
|
285
|
+
filteredData={mockFilteredData}
|
|
286
|
+
columns={mockColumns}
|
|
287
|
+
/>
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
291
|
+
await user.click(button)
|
|
292
|
+
|
|
293
|
+
const excelFilteredOptions = await screen.findAllByText(/Export Filtered.*2 rows/i)
|
|
294
|
+
await user.click(excelFilteredOptions[1])
|
|
295
|
+
|
|
296
|
+
await waitFor(() => {
|
|
297
|
+
expect(exportUtils.exportToExcel).toHaveBeenCalledWith(
|
|
298
|
+
mockFilteredData,
|
|
299
|
+
mockColumns,
|
|
300
|
+
expect.stringContaining('export_filtered')
|
|
301
|
+
)
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should export selected data to Excel', async () => {
|
|
306
|
+
const user = userEvent.setup()
|
|
307
|
+
render(
|
|
308
|
+
<ExportButton
|
|
309
|
+
data={mockData}
|
|
310
|
+
selectedData={mockSelectedData}
|
|
311
|
+
columns={mockColumns}
|
|
312
|
+
/>
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
316
|
+
await user.click(button)
|
|
317
|
+
|
|
318
|
+
const excelSelectedOptions = await screen.findAllByText(/Export Selected.*1 row/i)
|
|
319
|
+
await user.click(excelSelectedOptions[1])
|
|
320
|
+
|
|
321
|
+
await waitFor(() => {
|
|
322
|
+
expect(exportUtils.exportToExcel).toHaveBeenCalledWith(
|
|
323
|
+
mockSelectedData,
|
|
324
|
+
mockColumns,
|
|
325
|
+
expect.stringContaining('export_selected')
|
|
326
|
+
)
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
describe('Callbacks', () => {
|
|
332
|
+
it('should call onExportStart callback', async () => {
|
|
333
|
+
const user = userEvent.setup()
|
|
334
|
+
const onExportStart = jest.fn()
|
|
335
|
+
render(
|
|
336
|
+
<ExportButton
|
|
337
|
+
data={mockData}
|
|
338
|
+
columns={mockColumns}
|
|
339
|
+
onExportStart={onExportStart}
|
|
340
|
+
/>
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
344
|
+
await user.click(button)
|
|
345
|
+
|
|
346
|
+
const csvAllOptions = await screen.findAllByText(/Export All.*3 rows/i)
|
|
347
|
+
await user.click(csvAllOptions[0]) // First one is CSV
|
|
348
|
+
|
|
349
|
+
await waitFor(() => {
|
|
350
|
+
expect(onExportStart).toHaveBeenCalledWith('csv', 'all')
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('should call onExportComplete callback', async () => {
|
|
355
|
+
const user = userEvent.setup()
|
|
356
|
+
const onExportComplete = jest.fn()
|
|
357
|
+
render(
|
|
358
|
+
<ExportButton
|
|
359
|
+
data={mockData}
|
|
360
|
+
columns={mockColumns}
|
|
361
|
+
onExportComplete={onExportComplete}
|
|
362
|
+
/>
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
366
|
+
await user.click(button)
|
|
367
|
+
|
|
368
|
+
const csvAllOptions = await screen.findAllByText(/Export All.*3 rows/i)
|
|
369
|
+
await user.click(csvAllOptions[0]) // First one is CSV
|
|
370
|
+
|
|
371
|
+
await waitFor(() => {
|
|
372
|
+
expect(onExportComplete).toHaveBeenCalledWith('csv', 'all', 3)
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should call onExportError callback on error', async () => {
|
|
377
|
+
const user = userEvent.setup()
|
|
378
|
+
const onExportError = jest.fn()
|
|
379
|
+
const mockError = new Error('Export failed')
|
|
380
|
+
;(exportUtils.exportToCSV as jest.Mock).mockImplementation(() => {
|
|
381
|
+
throw mockError
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
render(
|
|
385
|
+
<ExportButton
|
|
386
|
+
data={mockData}
|
|
387
|
+
columns={mockColumns}
|
|
388
|
+
onExportError={onExportError}
|
|
389
|
+
/>
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
393
|
+
await user.click(button)
|
|
394
|
+
|
|
395
|
+
const csvAllOptions = await screen.findAllByText(/Export All.*3 rows/i)
|
|
396
|
+
await user.click(csvAllOptions[0]) // First one is CSV
|
|
397
|
+
|
|
398
|
+
await waitFor(() => {
|
|
399
|
+
expect(onExportError).toHaveBeenCalledWith(mockError)
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
describe('Error Handling', () => {
|
|
405
|
+
it('should handle export errors gracefully', async () => {
|
|
406
|
+
const user = userEvent.setup()
|
|
407
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation()
|
|
408
|
+
;(exportUtils.exportToCSV as jest.Mock).mockImplementation(() => {
|
|
409
|
+
throw new Error('Export failed')
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
render(<ExportButton data={mockData} columns={mockColumns} />)
|
|
413
|
+
|
|
414
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
415
|
+
await user.click(button)
|
|
416
|
+
|
|
417
|
+
const csvAllOptions = await screen.findAllByText(/Export All.*3 rows/i)
|
|
418
|
+
await user.click(csvAllOptions[0]) // First one is CSV
|
|
419
|
+
|
|
420
|
+
await waitFor(() => {
|
|
421
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
422
|
+
'Export failed:',
|
|
423
|
+
expect.any(Error)
|
|
424
|
+
)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
consoleError.mockRestore()
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('should reset loading state after error', async () => {
|
|
431
|
+
const user = userEvent.setup()
|
|
432
|
+
;(exportUtils.exportToCSV as jest.Mock).mockImplementation(() => {
|
|
433
|
+
throw new Error('Export failed')
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
render(<ExportButton data={mockData} columns={mockColumns} />)
|
|
437
|
+
|
|
438
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
439
|
+
await user.click(button)
|
|
440
|
+
|
|
441
|
+
const csvAllOptions = await screen.findAllByText(/Export All.*3 rows/i)
|
|
442
|
+
await user.click(csvAllOptions[0]) // First one is CSV
|
|
443
|
+
|
|
444
|
+
await waitFor(() => {
|
|
445
|
+
expect(screen.queryByText(/exporting/i)).not.toBeInTheDocument()
|
|
446
|
+
})
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
describe('Accessibility', () => {
|
|
451
|
+
it('should have proper aria-label', () => {
|
|
452
|
+
render(<ExportButton data={mockData} columns={mockColumns} />)
|
|
453
|
+
|
|
454
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
455
|
+
expect(button).toHaveAttribute('aria-label', 'Export data')
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('should be keyboard accessible', () => {
|
|
459
|
+
render(<ExportButton data={mockData} columns={mockColumns} />)
|
|
460
|
+
|
|
461
|
+
const button = screen.getByRole('button', { name: /export/i })
|
|
462
|
+
button.focus()
|
|
463
|
+
|
|
464
|
+
expect(button).toHaveFocus()
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { InlineEditCell } from '../../components/InlineEditCell'
|
|
4
|
+
|
|
5
|
+
describe('InlineEditCell', () => {
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
value: 'Test Value',
|
|
8
|
+
columnId: 'test-column',
|
|
9
|
+
rowId: 'test-row',
|
|
10
|
+
onSave: jest.fn().mockResolvedValue(undefined),
|
|
11
|
+
onCancel: jest.fn(),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('text input', () => {
|
|
19
|
+
it('should render with initial value', () => {
|
|
20
|
+
render(<InlineEditCell {...defaultProps} />)
|
|
21
|
+
|
|
22
|
+
const input = screen.getByRole('textbox')
|
|
23
|
+
expect(input).toHaveValue('Test Value')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should focus and select input on mount', () => {
|
|
27
|
+
render(<InlineEditCell {...defaultProps} />)
|
|
28
|
+
|
|
29
|
+
const input = screen.getByRole('textbox')
|
|
30
|
+
expect(document.activeElement).toBe(input)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should save on Enter key', async () => {
|
|
34
|
+
const user = userEvent.setup()
|
|
35
|
+
render(<InlineEditCell {...defaultProps} />)
|
|
36
|
+
|
|
37
|
+
const input = screen.getByRole('textbox')
|
|
38
|
+
await user.clear(input)
|
|
39
|
+
await user.type(input, 'New Value{Enter}')
|
|
40
|
+
|
|
41
|
+
expect(defaultProps.onSave).toHaveBeenCalledWith('New Value')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should cancel on Escape key', async () => {
|
|
45
|
+
const user = userEvent.setup()
|
|
46
|
+
render(<InlineEditCell {...defaultProps} />)
|
|
47
|
+
|
|
48
|
+
const input = screen.getByRole('textbox')
|
|
49
|
+
await user.type(input, '{Escape}')
|
|
50
|
+
|
|
51
|
+
expect(defaultProps.onCancel).toHaveBeenCalled()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should save on check button click', async () => {
|
|
55
|
+
const user = userEvent.setup()
|
|
56
|
+
render(<InlineEditCell {...defaultProps} />)
|
|
57
|
+
|
|
58
|
+
const buttons = screen.getAllByRole('button')
|
|
59
|
+
// First button is save (check), second is cancel (X)
|
|
60
|
+
await user.click(buttons[0])
|
|
61
|
+
|
|
62
|
+
expect(defaultProps.onSave).toHaveBeenCalledWith('Test Value')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should cancel on X button click', async () => {
|
|
66
|
+
const user = userEvent.setup()
|
|
67
|
+
render(<InlineEditCell {...defaultProps} />)
|
|
68
|
+
|
|
69
|
+
const buttons = screen.getAllByRole('button')
|
|
70
|
+
// Second button is cancel (X)
|
|
71
|
+
await user.click(buttons[1])
|
|
72
|
+
|
|
73
|
+
expect(defaultProps.onCancel).toHaveBeenCalled()
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('number input', () => {
|
|
78
|
+
it('should render number input when editType is number', () => {
|
|
79
|
+
render(<InlineEditCell {...defaultProps} value={42} editType="number" />)
|
|
80
|
+
|
|
81
|
+
const input = screen.getByRole('spinbutton')
|
|
82
|
+
expect(input).toHaveValue(42)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('validation', () => {
|
|
87
|
+
it('should show error message when validation fails', async () => {
|
|
88
|
+
const user = userEvent.setup()
|
|
89
|
+
const validate = jest.fn().mockReturnValue('Value is required')
|
|
90
|
+
|
|
91
|
+
render(
|
|
92
|
+
<InlineEditCell
|
|
93
|
+
{...defaultProps}
|
|
94
|
+
value=""
|
|
95
|
+
validate={validate}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const buttons = screen.getAllByRole('button')
|
|
100
|
+
await user.click(buttons[0]) // Save button
|
|
101
|
+
|
|
102
|
+
expect(screen.getByText('Value is required')).toBeInTheDocument()
|
|
103
|
+
expect(defaultProps.onSave).not.toHaveBeenCalled()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should save when validation passes', async () => {
|
|
107
|
+
const user = userEvent.setup()
|
|
108
|
+
const validate = jest.fn().mockReturnValue(null)
|
|
109
|
+
|
|
110
|
+
render(
|
|
111
|
+
<InlineEditCell
|
|
112
|
+
{...defaultProps}
|
|
113
|
+
validate={validate}
|
|
114
|
+
/>
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const buttons = screen.getAllByRole('button')
|
|
118
|
+
await user.click(buttons[0]) // Save button
|
|
119
|
+
|
|
120
|
+
expect(validate).toHaveBeenCalledWith('Test Value')
|
|
121
|
+
expect(defaultProps.onSave).toHaveBeenCalled()
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('select input', () => {
|
|
126
|
+
it('should render select when editType is select', () => {
|
|
127
|
+
render(
|
|
128
|
+
<InlineEditCell
|
|
129
|
+
{...defaultProps}
|
|
130
|
+
editType="select"
|
|
131
|
+
editOptions={['Option 1', 'Option 2', 'Option 3']}
|
|
132
|
+
value="Option 1"
|
|
133
|
+
/>
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Should have a trigger button
|
|
137
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('loading state', () => {
|
|
142
|
+
it('should show loading indicator while saving', async () => {
|
|
143
|
+
const slowSave = jest.fn().mockImplementation(
|
|
144
|
+
() => new Promise((resolve) => setTimeout(resolve, 100))
|
|
145
|
+
)
|
|
146
|
+
const user = userEvent.setup()
|
|
147
|
+
|
|
148
|
+
render(<InlineEditCell {...defaultProps} onSave={slowSave} />)
|
|
149
|
+
|
|
150
|
+
const buttons = screen.getAllByRole('button')
|
|
151
|
+
await user.click(buttons[0]) // Save button
|
|
152
|
+
|
|
153
|
+
// Should show loading state
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(buttons[0]).toBeDisabled()
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
})
|