foggy-data-viewer 1.0.1-beta.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.
Files changed (34) hide show
  1. package/README.md +273 -0
  2. package/dist/favicon.svg +4 -0
  3. package/dist/index.js +1531 -0
  4. package/dist/index.umd +1 -0
  5. package/dist/style.css +1 -0
  6. package/package.json +51 -0
  7. package/src/App.vue +469 -0
  8. package/src/api/viewer.ts +163 -0
  9. package/src/components/DataTable.test.ts +533 -0
  10. package/src/components/DataTable.vue +810 -0
  11. package/src/components/DataTableWithSearch.test.ts +628 -0
  12. package/src/components/DataTableWithSearch.vue +277 -0
  13. package/src/components/DataViewer.vue +310 -0
  14. package/src/components/SearchToolbar.test.ts +521 -0
  15. package/src/components/SearchToolbar.vue +406 -0
  16. package/src/components/composables/index.ts +2 -0
  17. package/src/components/composables/useTableSelection.test.ts +248 -0
  18. package/src/components/composables/useTableSelection.ts +44 -0
  19. package/src/components/composables/useTableSummary.test.ts +341 -0
  20. package/src/components/composables/useTableSummary.ts +129 -0
  21. package/src/components/filters/BoolFilter.vue +103 -0
  22. package/src/components/filters/DateRangeFilter.vue +194 -0
  23. package/src/components/filters/NumberRangeFilter.vue +160 -0
  24. package/src/components/filters/SelectFilter.vue +464 -0
  25. package/src/components/filters/TextFilter.vue +230 -0
  26. package/src/components/filters/index.ts +5 -0
  27. package/src/examples/EnhancedTableExample.vue +136 -0
  28. package/src/index.ts +32 -0
  29. package/src/main.ts +14 -0
  30. package/src/types/index.ts +159 -0
  31. package/src/utils/README.md +140 -0
  32. package/src/utils/schemaHelper.test.ts +215 -0
  33. package/src/utils/schemaHelper.ts +44 -0
  34. package/src/vite-env.d.ts +7 -0
@@ -0,0 +1,628 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import DataTableWithSearch from './DataTableWithSearch.vue'
4
+ import type { EnhancedColumnSchema, SliceRequestDef } from '@/types'
5
+
6
+ // Mock child components
7
+ vi.mock('./SearchToolbar.vue', () => ({
8
+ default: {
9
+ name: 'SearchToolbar',
10
+ template: '<div class="search-toolbar-mock"><slot /></div>',
11
+ props: ['columns', 'searchableFields', 'layout', 'showActions', 'modelValue'],
12
+ emits: ['update:modelValue', 'search', 'reset'],
13
+ methods: {
14
+ clearFilters() {
15
+ this.$emit('update:modelValue', [])
16
+ },
17
+ getFilters() {
18
+ return this.modelValue || []
19
+ }
20
+ }
21
+ }
22
+ }))
23
+
24
+ vi.mock('./DataTable.vue', () => ({
25
+ default: {
26
+ name: 'DataTable',
27
+ template: '<div class="data-table-mock"><slot /></div>',
28
+ props: ['columns', 'data', 'total', 'loading', 'pageSize', 'showFilters', 'initialSlice', 'serverSummary'],
29
+ emits: ['page-change', 'sort-change', 'filter-change', 'row-click', 'row-dblclick'],
30
+ methods: {
31
+ resetPagination() {
32
+ // mock
33
+ },
34
+ clearFilters() {
35
+ this.$emit('filter-change', [])
36
+ },
37
+ getGridInstance() {
38
+ return null
39
+ }
40
+ }
41
+ }
42
+ }))
43
+
44
+ describe('DataTableWithSearch', () => {
45
+ const mockColumns: EnhancedColumnSchema[] = [
46
+ { name: 'id', type: 'INTEGER', title: 'ID', filterable: true },
47
+ { name: 'name', type: 'TEXT', title: '名称', filterable: true },
48
+ { name: 'amount', type: 'MONEY', title: '金额', filterable: true, measure: true, aggregatable: true }
49
+ ]
50
+
51
+ const mockData = [
52
+ { id: 1, name: 'Test 1', amount: 100 },
53
+ { id: 2, name: 'Test 2', amount: 200 }
54
+ ]
55
+
56
+ const defaultProps = {
57
+ columns: mockColumns,
58
+ data: mockData,
59
+ total: 100,
60
+ loading: false
61
+ }
62
+
63
+ describe('Rendering', () => {
64
+ it('should render successfully', () => {
65
+ const wrapper = mount(DataTableWithSearch, {
66
+ props: defaultProps
67
+ })
68
+
69
+ expect(wrapper.exists()).toBe(true)
70
+ expect(wrapper.find('.data-table-with-search').exists()).toBe(true)
71
+ })
72
+
73
+ it('should render SearchToolbar by default', () => {
74
+ const wrapper = mount(DataTableWithSearch, {
75
+ props: defaultProps
76
+ })
77
+
78
+ expect(wrapper.find('.search-toolbar-mock').exists()).toBe(true)
79
+ })
80
+
81
+ it('should hide SearchToolbar when showSearchToolbar is false', () => {
82
+ const wrapper = mount(DataTableWithSearch, {
83
+ props: {
84
+ ...defaultProps,
85
+ showSearchToolbar: false
86
+ }
87
+ })
88
+
89
+ expect(wrapper.find('.search-toolbar-mock').exists()).toBe(false)
90
+ })
91
+
92
+ it('should always render DataTable', () => {
93
+ const wrapper = mount(DataTableWithSearch, {
94
+ props: defaultProps
95
+ })
96
+
97
+ expect(wrapper.find('.data-table-mock').exists()).toBe(true)
98
+ })
99
+ })
100
+
101
+ describe('Props Passthrough', () => {
102
+ it('should pass columns to both SearchToolbar and DataTable', () => {
103
+ const wrapper = mount(DataTableWithSearch, {
104
+ props: defaultProps
105
+ })
106
+
107
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
108
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
109
+
110
+ expect(searchToolbar.props('columns')).toEqual(mockColumns)
111
+ expect(dataTable.props('columns')).toEqual(mockColumns)
112
+ })
113
+
114
+ it('should pass data to DataTable', () => {
115
+ const wrapper = mount(DataTableWithSearch, {
116
+ props: defaultProps
117
+ })
118
+
119
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
120
+ expect(dataTable.props('data')).toEqual(mockData)
121
+ })
122
+
123
+ it('should pass total to DataTable', () => {
124
+ const wrapper = mount(DataTableWithSearch, {
125
+ props: {
126
+ ...defaultProps,
127
+ total: 500
128
+ }
129
+ })
130
+
131
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
132
+ expect(dataTable.props('total')).toBe(500)
133
+ })
134
+
135
+ it('should pass loading to DataTable', () => {
136
+ const wrapper = mount(DataTableWithSearch, {
137
+ props: {
138
+ ...defaultProps,
139
+ loading: true
140
+ }
141
+ })
142
+
143
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
144
+ expect(dataTable.props('loading')).toBe(true)
145
+ })
146
+
147
+ it('should pass pageSize to DataTable', () => {
148
+ const wrapper = mount(DataTableWithSearch, {
149
+ props: {
150
+ ...defaultProps,
151
+ pageSize: 100
152
+ }
153
+ })
154
+
155
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
156
+ expect(dataTable.props('pageSize')).toBe(100)
157
+ })
158
+
159
+ it('should pass showFilters to DataTable', () => {
160
+ const wrapper = mount(DataTableWithSearch, {
161
+ props: {
162
+ ...defaultProps,
163
+ showFilters: false
164
+ }
165
+ })
166
+
167
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
168
+ expect(dataTable.props('showFilters')).toBe(false)
169
+ })
170
+
171
+ it('should pass searchableFields to SearchToolbar', () => {
172
+ const searchableFields = ['name', 'amount']
173
+
174
+ const wrapper = mount(DataTableWithSearch, {
175
+ props: {
176
+ ...defaultProps,
177
+ searchableFields
178
+ }
179
+ })
180
+
181
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
182
+ expect(searchToolbar.props('searchableFields')).toEqual(searchableFields)
183
+ })
184
+
185
+ it('should pass searchLayout to SearchToolbar as layout prop', () => {
186
+ const wrapper = mount(DataTableWithSearch, {
187
+ props: {
188
+ ...defaultProps,
189
+ searchLayout: 'vertical'
190
+ }
191
+ })
192
+
193
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
194
+ expect(searchToolbar.props('layout')).toBe('vertical')
195
+ })
196
+
197
+ it('should pass showSearchActions to SearchToolbar as showActions prop', () => {
198
+ const wrapper = mount(DataTableWithSearch, {
199
+ props: {
200
+ ...defaultProps,
201
+ showSearchActions: false
202
+ }
203
+ })
204
+
205
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
206
+ expect(searchToolbar.props('showActions')).toBe(false)
207
+ })
208
+
209
+ it('should pass serverSummary to DataTable', () => {
210
+ const serverSummary = { total: 100, amount: 30000 }
211
+
212
+ const wrapper = mount(DataTableWithSearch, {
213
+ props: {
214
+ ...defaultProps,
215
+ serverSummary
216
+ }
217
+ })
218
+
219
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
220
+ expect(dataTable.props('serverSummary')).toEqual(serverSummary)
221
+ })
222
+ })
223
+
224
+ describe('Events', () => {
225
+ it('should emit page-change event from DataTable', async () => {
226
+ const wrapper = mount(DataTableWithSearch, {
227
+ props: defaultProps
228
+ })
229
+
230
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
231
+ await dataTable.vm.$emit('page-change', 2, 50)
232
+
233
+ expect(wrapper.emitted('page-change')).toBeTruthy()
234
+ expect(wrapper.emitted('page-change')![0]).toEqual([2, 50])
235
+ })
236
+
237
+ it('should emit sort-change event from DataTable', async () => {
238
+ const wrapper = mount(DataTableWithSearch, {
239
+ props: defaultProps
240
+ })
241
+
242
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
243
+ await dataTable.vm.$emit('sort-change', 'name', 'asc')
244
+
245
+ expect(wrapper.emitted('sort-change')).toBeTruthy()
246
+ expect(wrapper.emitted('sort-change')![0]).toEqual(['name', 'asc'])
247
+ })
248
+
249
+ it('should emit row-click event from DataTable', async () => {
250
+ const wrapper = mount(DataTableWithSearch, {
251
+ props: defaultProps
252
+ })
253
+
254
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
255
+ const mockRow = { id: 1, name: 'Test' }
256
+ const mockColumn = mockColumns[0]
257
+
258
+ await dataTable.vm.$emit('row-click', mockRow, mockColumn)
259
+
260
+ expect(wrapper.emitted('row-click')).toBeTruthy()
261
+ expect(wrapper.emitted('row-click')![0]).toEqual([mockRow, mockColumn])
262
+ })
263
+
264
+ it('should emit row-dblclick event from DataTable', async () => {
265
+ const wrapper = mount(DataTableWithSearch, {
266
+ props: defaultProps
267
+ })
268
+
269
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
270
+ const mockRow = { id: 1, name: 'Test' }
271
+ const mockColumn = mockColumns[0]
272
+
273
+ await dataTable.vm.$emit('row-dblclick', mockRow, mockColumn)
274
+
275
+ expect(wrapper.emitted('row-dblclick')).toBeTruthy()
276
+ expect(wrapper.emitted('row-dblclick')![0]).toEqual([mockRow, mockColumn])
277
+ })
278
+
279
+ it('should emit search event from SearchToolbar', async () => {
280
+ const wrapper = mount(DataTableWithSearch, {
281
+ props: defaultProps
282
+ })
283
+
284
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
285
+ await searchToolbar.vm.$emit('search')
286
+
287
+ expect(wrapper.emitted('search')).toBeTruthy()
288
+ })
289
+
290
+ it('should emit reset event from SearchToolbar', async () => {
291
+ const wrapper = mount(DataTableWithSearch, {
292
+ props: defaultProps
293
+ })
294
+
295
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
296
+ await searchToolbar.vm.$emit('reset')
297
+
298
+ expect(wrapper.emitted('reset')).toBeTruthy()
299
+ })
300
+ })
301
+
302
+ describe('Filter Merging', () => {
303
+ it('should merge search and table filters in merge mode', async () => {
304
+ const wrapper = mount(DataTableWithSearch, {
305
+ props: {
306
+ ...defaultProps,
307
+ filterMergeMode: 'merge'
308
+ }
309
+ })
310
+
311
+ const searchSlices: SliceRequestDef[] = [
312
+ { field: 'name', op: '=', value: 'test' }
313
+ ]
314
+
315
+ const tableSlices: SliceRequestDef[] = [
316
+ { field: 'amount', op: '>=', value: 100 }
317
+ ]
318
+
319
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
320
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
321
+
322
+ await searchToolbar.vm.$emit('update:modelValue', searchSlices)
323
+ await dataTable.vm.$emit('filter-change', tableSlices)
324
+
325
+ const filterChangeEvents = wrapper.emitted('filter-change')
326
+ expect(filterChangeEvents).toBeTruthy()
327
+
328
+ const lastEmit = filterChangeEvents![filterChangeEvents!.length - 1][0] as SliceRequestDef[]
329
+ expect(lastEmit).toHaveLength(2)
330
+ expect(lastEmit.find(s => s.field === 'name')).toBeTruthy()
331
+ expect(lastEmit.find(s => s.field === 'amount')).toBeTruthy()
332
+ })
333
+
334
+ it('should replace table filters with search filters in replace mode', async () => {
335
+ const wrapper = mount(DataTableWithSearch, {
336
+ props: {
337
+ ...defaultProps,
338
+ filterMergeMode: 'replace'
339
+ }
340
+ })
341
+
342
+ const searchSlices: SliceRequestDef[] = [
343
+ { field: 'name', op: '=', value: 'test' }
344
+ ]
345
+
346
+ const tableSlices: SliceRequestDef[] = [
347
+ { field: 'amount', op: '>=', value: 100 }
348
+ ]
349
+
350
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
351
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
352
+
353
+ // 先设置表头筛选
354
+ await dataTable.vm.$emit('filter-change', tableSlices)
355
+
356
+ // 再设置搜索工具栏筛选
357
+ await searchToolbar.vm.$emit('update:modelValue', searchSlices)
358
+
359
+ const filterChangeEvents = wrapper.emitted('filter-change')
360
+ const lastEmit = filterChangeEvents![filterChangeEvents!.length - 1][0] as SliceRequestDef[]
361
+
362
+ // replace 模式下,搜索工具栏筛选替换表头筛选
363
+ expect(lastEmit).toHaveLength(1)
364
+ expect(lastEmit[0].field).toBe('name')
365
+ })
366
+
367
+ it('should use table filters when search filters are empty in replace mode', async () => {
368
+ const wrapper = mount(DataTableWithSearch, {
369
+ props: {
370
+ ...defaultProps,
371
+ filterMergeMode: 'replace'
372
+ }
373
+ })
374
+
375
+ const tableSlices: SliceRequestDef[] = [
376
+ { field: 'amount', op: '>=', value: 100 }
377
+ ]
378
+
379
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
380
+ await dataTable.vm.$emit('filter-change', tableSlices)
381
+
382
+ const filterChangeEvents = wrapper.emitted('filter-change')
383
+ const lastEmit = filterChangeEvents![filterChangeEvents!.length - 1][0] as SliceRequestDef[]
384
+
385
+ expect(lastEmit).toEqual(tableSlices)
386
+ })
387
+
388
+ it('should not duplicate filters for same field in merge mode', async () => {
389
+ const wrapper = mount(DataTableWithSearch, {
390
+ props: {
391
+ ...defaultProps,
392
+ filterMergeMode: 'merge'
393
+ }
394
+ })
395
+
396
+ const searchSlices: SliceRequestDef[] = [
397
+ { field: 'name', op: '=', value: 'from-search' }
398
+ ]
399
+
400
+ const tableSlices: SliceRequestDef[] = [
401
+ { field: 'name', op: '=', value: 'from-table' }
402
+ ]
403
+
404
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
405
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
406
+
407
+ await searchToolbar.vm.$emit('update:modelValue', searchSlices)
408
+ await dataTable.vm.$emit('filter-change', tableSlices)
409
+
410
+ const filterChangeEvents = wrapper.emitted('filter-change')
411
+ const lastEmit = filterChangeEvents![filterChangeEvents!.length - 1][0] as SliceRequestDef[]
412
+
413
+ // 同一字段只保留搜索工具栏的筛选
414
+ expect(lastEmit).toHaveLength(1)
415
+ expect(lastEmit[0].value).toBe('from-search')
416
+ })
417
+ })
418
+
419
+ describe('Exposed Methods', () => {
420
+ it('should expose getSearchToolbar method', () => {
421
+ const wrapper = mount(DataTableWithSearch, {
422
+ props: defaultProps
423
+ })
424
+
425
+ const vm = wrapper.vm as any
426
+ expect(vm.getSearchToolbar).toBeDefined()
427
+
428
+ const searchToolbar = vm.getSearchToolbar()
429
+ expect(searchToolbar).toBeDefined()
430
+ })
431
+
432
+ it('should expose getDataTable method', () => {
433
+ const wrapper = mount(DataTableWithSearch, {
434
+ props: defaultProps
435
+ })
436
+
437
+ const vm = wrapper.vm as any
438
+ expect(vm.getDataTable).toBeDefined()
439
+
440
+ const dataTable = vm.getDataTable()
441
+ expect(dataTable).toBeDefined()
442
+ })
443
+
444
+ it('should expose clearSearchFilters method', () => {
445
+ const wrapper = mount(DataTableWithSearch, {
446
+ props: defaultProps
447
+ })
448
+
449
+ const vm = wrapper.vm as any
450
+ expect(vm.clearSearchFilters).toBeDefined()
451
+
452
+ vm.clearSearchFilters()
453
+
454
+ const filterChangeEvents = wrapper.emitted('filter-change')
455
+ expect(filterChangeEvents).toBeTruthy()
456
+ })
457
+
458
+ it('should expose clearTableFilters method', () => {
459
+ const wrapper = mount(DataTableWithSearch, {
460
+ props: defaultProps
461
+ })
462
+
463
+ const vm = wrapper.vm as any
464
+ expect(vm.clearTableFilters).toBeDefined()
465
+
466
+ vm.clearTableFilters()
467
+
468
+ const filterChangeEvents = wrapper.emitted('filter-change')
469
+ expect(filterChangeEvents).toBeTruthy()
470
+ })
471
+
472
+ it('should expose clearAllFilters method', () => {
473
+ const wrapper = mount(DataTableWithSearch, {
474
+ props: defaultProps
475
+ })
476
+
477
+ const vm = wrapper.vm as any
478
+ expect(vm.clearAllFilters).toBeDefined()
479
+
480
+ vm.clearAllFilters()
481
+
482
+ const filterChangeEvents = wrapper.emitted('filter-change')
483
+ expect(filterChangeEvents).toBeTruthy()
484
+ })
485
+
486
+ it('should expose getMergedFilters method', () => {
487
+ const wrapper = mount(DataTableWithSearch, {
488
+ props: defaultProps
489
+ })
490
+
491
+ const vm = wrapper.vm as any
492
+ expect(vm.getMergedFilters).toBeDefined()
493
+
494
+ const filters = vm.getMergedFilters()
495
+ expect(Array.isArray(filters)).toBe(true)
496
+ })
497
+
498
+ it('should expose resetPagination method', () => {
499
+ const wrapper = mount(DataTableWithSearch, {
500
+ props: defaultProps
501
+ })
502
+
503
+ const vm = wrapper.vm as any
504
+ expect(vm.resetPagination).toBeDefined()
505
+
506
+ // 不应该报错
507
+ vm.resetPagination()
508
+ })
509
+ })
510
+
511
+ describe('Integration', () => {
512
+ it('should work in complete workflow', async () => {
513
+ const wrapper = mount(DataTableWithSearch, {
514
+ props: defaultProps
515
+ })
516
+
517
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
518
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
519
+
520
+ // 1. 设置搜索工具栏筛选
521
+ await searchToolbar.vm.$emit('update:modelValue', [
522
+ { field: 'name', op: '=', value: 'test' }
523
+ ])
524
+
525
+ expect(wrapper.emitted('filter-change')).toBeTruthy()
526
+
527
+ // 2. 设置表头筛选
528
+ await dataTable.vm.$emit('filter-change', [
529
+ { field: 'amount', op: '>=', value: 100 }
530
+ ])
531
+
532
+ // 3. 点击搜索按钮
533
+ await searchToolbar.vm.$emit('search')
534
+ expect(wrapper.emitted('search')).toBeTruthy()
535
+
536
+ // 4. 点击重置按钮
537
+ await searchToolbar.vm.$emit('reset')
538
+ expect(wrapper.emitted('reset')).toBeTruthy()
539
+
540
+ // 5. 分页变化
541
+ await dataTable.vm.$emit('page-change', 2, 50)
542
+ expect(wrapper.emitted('page-change')).toBeTruthy()
543
+ })
544
+
545
+ it('should handle rapid filter changes', async () => {
546
+ const wrapper = mount(DataTableWithSearch, {
547
+ props: defaultProps
548
+ })
549
+
550
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
551
+
552
+ // 快速连续更新
553
+ for (let i = 1; i <= 5; i++) {
554
+ await searchToolbar.vm.$emit('update:modelValue', [
555
+ { field: 'name', op: '=', value: `test${i}` }
556
+ ])
557
+ }
558
+
559
+ const filterChangeEvents = wrapper.emitted('filter-change')
560
+ expect(filterChangeEvents).toBeTruthy()
561
+ expect(filterChangeEvents!.length).toBe(5)
562
+ })
563
+ })
564
+
565
+ describe('Edge Cases', () => {
566
+ it('should handle undefined searchableFields', () => {
567
+ const wrapper = mount(DataTableWithSearch, {
568
+ props: {
569
+ ...defaultProps,
570
+ searchableFields: undefined
571
+ }
572
+ })
573
+
574
+ const searchToolbar = wrapper.findComponent({ name: 'SearchToolbar' })
575
+ expect(searchToolbar.props('searchableFields')).toBeUndefined()
576
+ })
577
+
578
+ it('should handle empty data array', () => {
579
+ const wrapper = mount(DataTableWithSearch, {
580
+ props: {
581
+ ...defaultProps,
582
+ data: []
583
+ }
584
+ })
585
+
586
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
587
+ expect(dataTable.props('data')).toEqual([])
588
+ })
589
+
590
+ it('should handle zero total', () => {
591
+ const wrapper = mount(DataTableWithSearch, {
592
+ props: {
593
+ ...defaultProps,
594
+ total: 0
595
+ }
596
+ })
597
+
598
+ const dataTable = wrapper.findComponent({ name: 'DataTable' })
599
+ expect(dataTable.props('total')).toBe(0)
600
+ })
601
+
602
+ it('should handle empty columns array', () => {
603
+ const wrapper = mount(DataTableWithSearch, {
604
+ props: {
605
+ ...defaultProps,
606
+ columns: []
607
+ }
608
+ })
609
+
610
+ expect(wrapper.exists()).toBe(true)
611
+ })
612
+ })
613
+
614
+ describe('Slot Passthrough', () => {
615
+ it('should have slot passthrough capability', () => {
616
+ const wrapper = mount(DataTableWithSearch, {
617
+ props: defaultProps,
618
+ slots: {
619
+ toolbar: '<div class="custom-toolbar">Custom Toolbar</div>'
620
+ }
621
+ })
622
+
623
+ // 插槽会传递给 DataTable,但由于使用了 mock 组件,无法直接验证
624
+ // 这里只验证组件能够接受插槽而不报错
625
+ expect(wrapper.exists()).toBe(true)
626
+ })
627
+ })
628
+ })