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.
- package/README.md +273 -0
- package/dist/favicon.svg +4 -0
- package/dist/index.js +1531 -0
- package/dist/index.umd +1 -0
- package/dist/style.css +1 -0
- package/package.json +51 -0
- package/src/App.vue +469 -0
- package/src/api/viewer.ts +163 -0
- package/src/components/DataTable.test.ts +533 -0
- package/src/components/DataTable.vue +810 -0
- package/src/components/DataTableWithSearch.test.ts +628 -0
- package/src/components/DataTableWithSearch.vue +277 -0
- package/src/components/DataViewer.vue +310 -0
- package/src/components/SearchToolbar.test.ts +521 -0
- package/src/components/SearchToolbar.vue +406 -0
- package/src/components/composables/index.ts +2 -0
- package/src/components/composables/useTableSelection.test.ts +248 -0
- package/src/components/composables/useTableSelection.ts +44 -0
- package/src/components/composables/useTableSummary.test.ts +341 -0
- package/src/components/composables/useTableSummary.ts +129 -0
- package/src/components/filters/BoolFilter.vue +103 -0
- package/src/components/filters/DateRangeFilter.vue +194 -0
- package/src/components/filters/NumberRangeFilter.vue +160 -0
- package/src/components/filters/SelectFilter.vue +464 -0
- package/src/components/filters/TextFilter.vue +230 -0
- package/src/components/filters/index.ts +5 -0
- package/src/examples/EnhancedTableExample.vue +136 -0
- package/src/index.ts +32 -0
- package/src/main.ts +14 -0
- package/src/types/index.ts +159 -0
- package/src/utils/README.md +140 -0
- package/src/utils/schemaHelper.test.ts +215 -0
- package/src/utils/schemaHelper.ts +44 -0
- 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
|
+
})
|