slim-select 2.13.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -44
- package/dist/index.d.ts +304 -52
- package/dist/react/index.d.ts +304 -0
- package/dist/react/index.js +1549 -0
- package/dist/react/index.umd.js +22 -0
- package/dist/slimselect.cjs.js +2 -2030
- package/dist/slimselect.cjs.js.map +1 -0
- package/dist/slimselect.css +1 -1
- package/dist/slimselect.css.map +1 -1
- package/dist/slimselect.es.js +1225 -2012
- package/dist/slimselect.es.js.map +1 -0
- package/dist/slimselect.iife.js +2 -0
- package/dist/slimselect.iife.js.map +1 -0
- package/dist/slimselect.js +2 -2036
- package/dist/slimselect.js.map +1 -0
- package/dist/vue/index.d.ts +304 -0
- package/dist/vue/index.js +1341 -0
- package/dist/vue/index.umd.js +1 -0
- package/index.html +1 -0
- package/package.json +63 -40
- package/release.js +0 -7
- package/src/slim-select/accessibility.test.ts +782 -0
- package/src/slim-select/classes.test.ts +7 -6
- package/src/slim-select/classes.ts +8 -6
- package/src/slim-select/helpers.test.ts +10 -8
- package/src/slim-select/index.test.ts +509 -14
- package/src/slim-select/index.ts +58 -31
- package/src/slim-select/react/index.tsx +9 -0
- package/src/slim-select/react/react.tsx +112 -0
- package/src/slim-select/render.test.ts +479 -119
- package/src/slim-select/render.ts +213 -91
- package/src/slim-select/select.test.ts +28 -10
- package/src/slim-select/select.ts +35 -12
- package/src/slim-select/settings.test.ts +19 -8
- package/src/slim-select/settings.ts +2 -4
- package/src/slim-select/slimselect.scss +30 -15
- package/src/slim-select/store.test.ts +13 -16
- package/src/slim-select/store.ts +97 -86
- package/src/slim-select/vue/index.ts +9 -0
- package/src/slim-select/vue/vue.test.ts +832 -0
- package/src/slim-select/{vue.vue → vue/vue.vue} +45 -23
- package/tsconfig.json +6 -4
- package/{vite.config.mts → vite.config.docs.mts} +10 -0
- package/vite.config.lib.mts +48 -0
- package/vite.config.react.mts +40 -0
- package/vite.config.vue.mts +39 -0
- package/vitest.config.ts +29 -0
- package/dist/classes.d.ts +0 -43
- package/dist/helpers.d.ts +0 -5
- package/dist/render.d.ts +0 -81
- package/dist/select.d.ts +0 -30
- package/dist/settings.d.ts +0 -35
- package/dist/slimselect.global.js +0 -2033
- package/dist/slimselect.min.js +0 -1
- package/dist/slimselect.umd.js +0 -2036
- package/dist/slimselect.umd.min.js +0 -1
- package/dist/store.d.ts +0 -84
- package/jest.config.js +0 -14
- package/src/slim-select/rollup.config.mjs +0 -127
- package/src/slim-select/tsconfig.json +0 -14
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import { mount, VueWrapper } from '@vue/test-utils'
|
|
3
|
+
import { nextTick, PropType } from 'vue'
|
|
4
|
+
import SlimSelect, { Option, Events } from '@/slim-select'
|
|
5
|
+
import SlimSelectVue from './vue.vue'
|
|
6
|
+
|
|
7
|
+
describe('SlimSelect Vue Component', () => {
|
|
8
|
+
let wrapper: VueWrapper<any>
|
|
9
|
+
let consoleInfoSpy: ReturnType<typeof vi.spyOn>
|
|
10
|
+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Mock console methods
|
|
14
|
+
consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
|
|
15
|
+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
if (wrapper) {
|
|
20
|
+
wrapper.unmount()
|
|
21
|
+
}
|
|
22
|
+
// Restore console methods
|
|
23
|
+
consoleInfoSpy.mockRestore()
|
|
24
|
+
consoleWarnSpy.mockRestore()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('Basic Rendering', () => {
|
|
28
|
+
test('renders a select element', () => {
|
|
29
|
+
wrapper = mount(SlimSelectVue)
|
|
30
|
+
const select = wrapper.find('select')
|
|
31
|
+
expect(select.exists()).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('renders with single select by default', () => {
|
|
35
|
+
wrapper = mount(SlimSelectVue)
|
|
36
|
+
const select = wrapper.find('select')
|
|
37
|
+
expect(select.attributes('multiple')).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('renders with multiple attribute when multiple prop is true', () => {
|
|
41
|
+
wrapper = mount(SlimSelectVue, {
|
|
42
|
+
props: {
|
|
43
|
+
multiple: true
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
const select = wrapper.find('select')
|
|
47
|
+
expect(select.attributes('multiple')).toBeDefined()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('Slot Content', () => {
|
|
52
|
+
test('renders slot content', () => {
|
|
53
|
+
wrapper = mount(SlimSelectVue, {
|
|
54
|
+
slots: {
|
|
55
|
+
default: `
|
|
56
|
+
<option value="1">Option 1</option>
|
|
57
|
+
<option value="2">Option 2</option>
|
|
58
|
+
`
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const options = wrapper.findAll('option')
|
|
63
|
+
expect(options).toHaveLength(2)
|
|
64
|
+
expect(options[0].text()).toBe('Option 1')
|
|
65
|
+
expect(options[1].text()).toBe('Option 2')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('shows warning when both slot and data prop are provided', () => {
|
|
69
|
+
wrapper = mount(SlimSelectVue, {
|
|
70
|
+
props: {
|
|
71
|
+
data: [{ value: '1', text: 'Data Option' }]
|
|
72
|
+
},
|
|
73
|
+
slots: {
|
|
74
|
+
default: '<option value="2">Slot Option</option>'
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
79
|
+
expect.stringContaining('Both slot content and data prop are provided')
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('reactive slot content WITHOUT :selected attribute works with v-model', async () => {
|
|
84
|
+
// Test that v-model auto-syncs without needing :selected
|
|
85
|
+
const TestComponent = {
|
|
86
|
+
components: { SlimSelectVue },
|
|
87
|
+
template: `
|
|
88
|
+
<SlimSelectVue v-model="selected" multiple>
|
|
89
|
+
<option v-for="opt in options" :key="opt.value" :value="opt.value">
|
|
90
|
+
{{ opt.text }}
|
|
91
|
+
</option>
|
|
92
|
+
</SlimSelectVue>
|
|
93
|
+
`,
|
|
94
|
+
data() {
|
|
95
|
+
return {
|
|
96
|
+
selected: ['v1'] as string[],
|
|
97
|
+
options: [
|
|
98
|
+
{ value: 'v1', text: 'Value 1' },
|
|
99
|
+
{ value: 'v2', text: 'Value 2' },
|
|
100
|
+
{ value: 'v3', text: 'Value 3' }
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const wrapper = mount(TestComponent)
|
|
107
|
+
await nextTick()
|
|
108
|
+
|
|
109
|
+
// Verify initial selection works without :selected
|
|
110
|
+
const slimComponent = wrapper.findComponent(SlimSelectVue)
|
|
111
|
+
const slim = (slimComponent.vm as any).slim as SlimSelect
|
|
112
|
+
expect(slim.getSelected()).toEqual(['v1'])
|
|
113
|
+
|
|
114
|
+
// Parent component updates selected
|
|
115
|
+
;(wrapper.vm.selected as string[]) = ['v2', 'v3']
|
|
116
|
+
await nextTick()
|
|
117
|
+
|
|
118
|
+
// Verify SlimSelect reflects the change
|
|
119
|
+
expect(slim.getSelected()).toEqual(['v2', 'v3'])
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('v-model binding', () => {
|
|
124
|
+
test('accepts initial modelValue for single select', async () => {
|
|
125
|
+
wrapper = mount(SlimSelectVue, {
|
|
126
|
+
props: {
|
|
127
|
+
modelValue: '2',
|
|
128
|
+
data: [
|
|
129
|
+
{ value: '1', text: 'Option 1' },
|
|
130
|
+
{ value: '2', text: 'Option 2' },
|
|
131
|
+
{ value: '3', text: 'Option 3' }
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
await nextTick()
|
|
137
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
138
|
+
const selected = slim.getSelected()
|
|
139
|
+
expect(selected[0]).toBe('2')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('accepts initial modelValue for multiple select', async () => {
|
|
143
|
+
wrapper = mount(SlimSelectVue, {
|
|
144
|
+
props: {
|
|
145
|
+
modelValue: ['2', '3'],
|
|
146
|
+
multiple: true,
|
|
147
|
+
data: [
|
|
148
|
+
{ value: '1', text: 'Option 1' },
|
|
149
|
+
{ value: '2', text: 'Option 2' },
|
|
150
|
+
{ value: '3', text: 'Option 3' }
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
await nextTick()
|
|
156
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
157
|
+
const selected = slim.getSelected()
|
|
158
|
+
expect(selected).toEqual(['2', '3'])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('emits update:modelValue when selection changes', async () => {
|
|
162
|
+
wrapper = mount(SlimSelectVue, {
|
|
163
|
+
props: {
|
|
164
|
+
modelValue: '1',
|
|
165
|
+
data: [
|
|
166
|
+
{ value: '1', text: 'Option 1' },
|
|
167
|
+
{ value: '2', text: 'Option 2' }
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
await nextTick()
|
|
173
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
174
|
+
|
|
175
|
+
// Simulate selection change
|
|
176
|
+
slim.setSelected('2')
|
|
177
|
+
await nextTick()
|
|
178
|
+
|
|
179
|
+
const emitted = wrapper.emitted('update:modelValue')
|
|
180
|
+
expect(emitted).toBeTruthy()
|
|
181
|
+
expect(emitted?.[0]).toEqual(['2'])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('updates selection when modelValue prop changes', async () => {
|
|
185
|
+
wrapper = mount(SlimSelectVue, {
|
|
186
|
+
props: {
|
|
187
|
+
modelValue: '1',
|
|
188
|
+
data: [
|
|
189
|
+
{ value: '1', text: 'Option 1' },
|
|
190
|
+
{ value: '2', text: 'Option 2' }
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
await nextTick()
|
|
196
|
+
|
|
197
|
+
// Update modelValue
|
|
198
|
+
await wrapper.setProps({ modelValue: '2' })
|
|
199
|
+
await nextTick()
|
|
200
|
+
|
|
201
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
202
|
+
const selected = slim.getSelected()
|
|
203
|
+
expect(selected[0]).toBe('2')
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('Data Prop', () => {
|
|
208
|
+
test('initializes with data prop', async () => {
|
|
209
|
+
const testData = [
|
|
210
|
+
{ value: '1', text: 'Option 1' },
|
|
211
|
+
{ value: '2', text: 'Option 2' },
|
|
212
|
+
{ value: '3', text: 'Option 3' }
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
wrapper = mount(SlimSelectVue, {
|
|
216
|
+
props: {
|
|
217
|
+
data: testData
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
await nextTick()
|
|
222
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
223
|
+
const data = slim.getData()
|
|
224
|
+
|
|
225
|
+
expect(data).toHaveLength(3)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('updates data when data prop changes', async () => {
|
|
229
|
+
wrapper = mount(SlimSelectVue, {
|
|
230
|
+
props: {
|
|
231
|
+
data: [
|
|
232
|
+
{ value: '1', text: 'Option 1' },
|
|
233
|
+
{ value: '2', text: 'Option 2' }
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
await nextTick()
|
|
239
|
+
|
|
240
|
+
// Update data prop
|
|
241
|
+
await wrapper.setProps({
|
|
242
|
+
data: [
|
|
243
|
+
{ value: '3', text: 'Option 3' },
|
|
244
|
+
{ value: '4', text: 'Option 4' },
|
|
245
|
+
{ value: '5', text: 'Option 5' }
|
|
246
|
+
]
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
await nextTick()
|
|
250
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
251
|
+
const data = slim.getData()
|
|
252
|
+
|
|
253
|
+
expect(data).toHaveLength(3)
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe('Settings Prop', () => {
|
|
258
|
+
test('applies settings prop', async () => {
|
|
259
|
+
wrapper = mount(SlimSelectVue, {
|
|
260
|
+
props: {
|
|
261
|
+
data: [{ value: '1', text: 'Option 1' }],
|
|
262
|
+
settings: {
|
|
263
|
+
showSearch: false,
|
|
264
|
+
placeholderText: 'Custom Placeholder'
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
await nextTick()
|
|
270
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
271
|
+
|
|
272
|
+
expect(slim.settings.showSearch).toBe(false)
|
|
273
|
+
expect(slim.settings.placeholderText).toBe('Custom Placeholder')
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
describe('Events Prop', () => {
|
|
278
|
+
test('calls afterChange event from events prop', async () => {
|
|
279
|
+
const afterChangeMock = vi.fn()
|
|
280
|
+
|
|
281
|
+
wrapper = mount(SlimSelectVue, {
|
|
282
|
+
props: {
|
|
283
|
+
modelValue: '1',
|
|
284
|
+
data: [
|
|
285
|
+
{ value: '1', text: 'Option 1' },
|
|
286
|
+
{ value: '2', text: 'Option 2' }
|
|
287
|
+
],
|
|
288
|
+
events: {
|
|
289
|
+
afterChange: afterChangeMock
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
await nextTick()
|
|
295
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
296
|
+
|
|
297
|
+
// Trigger a change
|
|
298
|
+
slim.setSelected('2')
|
|
299
|
+
await nextTick()
|
|
300
|
+
|
|
301
|
+
expect(afterChangeMock).toHaveBeenCalled()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('calls both custom afterChange and emits update:modelValue', async () => {
|
|
305
|
+
const afterChangeMock = vi.fn()
|
|
306
|
+
|
|
307
|
+
wrapper = mount(SlimSelectVue, {
|
|
308
|
+
props: {
|
|
309
|
+
modelValue: '1',
|
|
310
|
+
data: [
|
|
311
|
+
{ value: '1', text: 'Option 1' },
|
|
312
|
+
{ value: '2', text: 'Option 2' }
|
|
313
|
+
],
|
|
314
|
+
events: {
|
|
315
|
+
afterChange: afterChangeMock
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
await nextTick()
|
|
321
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
322
|
+
|
|
323
|
+
// Trigger a change
|
|
324
|
+
slim.setSelected('2')
|
|
325
|
+
await nextTick()
|
|
326
|
+
|
|
327
|
+
// Both should be called
|
|
328
|
+
expect(afterChangeMock).toHaveBeenCalled()
|
|
329
|
+
const emitted = wrapper.emitted('update:modelValue')
|
|
330
|
+
expect(emitted).toBeTruthy()
|
|
331
|
+
expect(emitted?.[0]).toEqual(['2'])
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
describe('Component Methods', () => {
|
|
336
|
+
test('getSlimSelect returns SlimSelect instance', async () => {
|
|
337
|
+
wrapper = mount(SlimSelectVue, {
|
|
338
|
+
props: {
|
|
339
|
+
data: [{ value: '1', text: 'Option 1' }]
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
await nextTick()
|
|
344
|
+
const slimInstance = (wrapper.vm as any).getSlimSelect()
|
|
345
|
+
|
|
346
|
+
expect(slimInstance).toBeInstanceOf(SlimSelect)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('getCleanValue handles string values correctly', () => {
|
|
350
|
+
wrapper = mount(SlimSelectVue, {
|
|
351
|
+
props: {
|
|
352
|
+
multiple: false
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const result = (wrapper.vm as any).getCleanValue('test')
|
|
357
|
+
expect(result).toBe('test')
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
test('getCleanValue converts string to array for multiple select', () => {
|
|
361
|
+
wrapper = mount(SlimSelectVue, {
|
|
362
|
+
props: {
|
|
363
|
+
multiple: true
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const result = (wrapper.vm as any).getCleanValue('test')
|
|
368
|
+
expect(result).toEqual(['test'])
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
test('getCleanValue converts array to string for single select', () => {
|
|
372
|
+
wrapper = mount(SlimSelectVue, {
|
|
373
|
+
props: {
|
|
374
|
+
multiple: false
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
const result = (wrapper.vm as any).getCleanValue(['test1', 'test2'])
|
|
379
|
+
expect(result).toBe('test1')
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test('getCleanValue returns empty array for undefined value in multiple select', () => {
|
|
383
|
+
wrapper = mount(SlimSelectVue, {
|
|
384
|
+
props: {
|
|
385
|
+
multiple: true
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
const result = (wrapper.vm as any).getCleanValue(undefined)
|
|
390
|
+
expect(result).toEqual([])
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('getCleanValue returns empty string for undefined value in single select', () => {
|
|
394
|
+
wrapper = mount(SlimSelectVue, {
|
|
395
|
+
props: {
|
|
396
|
+
multiple: false
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const result = (wrapper.vm as any).getCleanValue(undefined)
|
|
401
|
+
expect(result).toBe('')
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
describe('Lifecycle', () => {
|
|
406
|
+
test('initializes SlimSelect on mount', async () => {
|
|
407
|
+
wrapper = mount(SlimSelectVue, {
|
|
408
|
+
props: {
|
|
409
|
+
data: [{ value: '1', text: 'Option 1' }]
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
await nextTick()
|
|
414
|
+
const slim = (wrapper.vm as any).slim
|
|
415
|
+
|
|
416
|
+
expect(slim).not.toBeNull()
|
|
417
|
+
expect(slim).toBeInstanceOf(SlimSelect)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test('destroys SlimSelect on unmount', async () => {
|
|
421
|
+
wrapper = mount(SlimSelectVue, {
|
|
422
|
+
props: {
|
|
423
|
+
data: [{ value: '1', text: 'Option 1' }]
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
await nextTick()
|
|
428
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
429
|
+
const destroySpy = vi.spyOn(slim, 'destroy')
|
|
430
|
+
|
|
431
|
+
wrapper.unmount()
|
|
432
|
+
|
|
433
|
+
expect(destroySpy).toHaveBeenCalled()
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
describe('Value Type Handling', () => {
|
|
438
|
+
test('handles empty string as modelValue for single select', async () => {
|
|
439
|
+
wrapper = mount(SlimSelectVue, {
|
|
440
|
+
props: {
|
|
441
|
+
modelValue: '',
|
|
442
|
+
data: [
|
|
443
|
+
{ value: '1', text: 'Option 1' },
|
|
444
|
+
{ value: '2', text: 'Option 2' }
|
|
445
|
+
]
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
await nextTick()
|
|
450
|
+
expect(wrapper.props('modelValue')).toBe('')
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
test('handles empty array as modelValue for multiple select', async () => {
|
|
454
|
+
wrapper = mount(SlimSelectVue, {
|
|
455
|
+
props: {
|
|
456
|
+
modelValue: [],
|
|
457
|
+
multiple: true,
|
|
458
|
+
data: [
|
|
459
|
+
{ value: '1', text: 'Option 1' },
|
|
460
|
+
{ value: '2', text: 'Option 2' }
|
|
461
|
+
]
|
|
462
|
+
}
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
await nextTick()
|
|
466
|
+
expect(wrapper.props('modelValue')).toEqual([])
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
describe('Reactivity Edge Cases', () => {
|
|
471
|
+
test('does not cause conflicts with v-once on slot', async () => {
|
|
472
|
+
// This test ensures v-once prevents reactivity issues
|
|
473
|
+
wrapper = mount(SlimSelectVue, {
|
|
474
|
+
slots: {
|
|
475
|
+
default: `
|
|
476
|
+
<option value="1">Option 1</option>
|
|
477
|
+
<option value="2">Option 2</option>
|
|
478
|
+
`
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
await nextTick()
|
|
483
|
+
|
|
484
|
+
// The slot should render only once
|
|
485
|
+
const initialOptions = wrapper.findAll('option')
|
|
486
|
+
expect(initialOptions).toHaveLength(2)
|
|
487
|
+
|
|
488
|
+
// SlimSelect should be initialized without errors
|
|
489
|
+
const slim = (wrapper.vm as any).slim
|
|
490
|
+
expect(slim).toBeInstanceOf(SlimSelect)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
test('data prop changes are reactive', async () => {
|
|
494
|
+
const initialData = [
|
|
495
|
+
{ value: '1', text: 'Option 1' },
|
|
496
|
+
{ value: '2', text: 'Option 2' }
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
wrapper = mount(SlimSelectVue, {
|
|
500
|
+
props: {
|
|
501
|
+
data: initialData
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
await nextTick()
|
|
506
|
+
let slim = (wrapper.vm as any).slim as SlimSelect
|
|
507
|
+
let data = slim.getData()
|
|
508
|
+
expect(data).toHaveLength(2)
|
|
509
|
+
|
|
510
|
+
// Update data prop
|
|
511
|
+
const newData = [
|
|
512
|
+
{ value: '3', text: 'Option 3' },
|
|
513
|
+
{ value: '4', text: 'Option 4' },
|
|
514
|
+
{ value: '5', text: 'Option 5' }
|
|
515
|
+
]
|
|
516
|
+
|
|
517
|
+
await wrapper.setProps({ data: newData })
|
|
518
|
+
await nextTick()
|
|
519
|
+
|
|
520
|
+
slim = (wrapper.vm as any).slim as SlimSelect
|
|
521
|
+
data = slim.getData()
|
|
522
|
+
expect(data).toHaveLength(3)
|
|
523
|
+
})
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
describe('Complex Parent-Child Reactivity Scenario', () => {
|
|
527
|
+
test('handles computed props from parent with bidirectional v-model updates', async () => {
|
|
528
|
+
// Simulate a parent component with computed values based on modelValue
|
|
529
|
+
// Similar to the CustomFields use case
|
|
530
|
+
const parentModelValue = {
|
|
531
|
+
field1: ['option1', 'option2'],
|
|
532
|
+
field2: ['option3']
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const updateCallback = vi.fn()
|
|
536
|
+
|
|
537
|
+
// Create wrapper with data computed from parent's modelValue
|
|
538
|
+
wrapper = mount(SlimSelectVue, {
|
|
539
|
+
props: {
|
|
540
|
+
modelValue: parentModelValue.field1,
|
|
541
|
+
multiple: true,
|
|
542
|
+
data: [
|
|
543
|
+
{ value: 'option1', text: 'Option 1' },
|
|
544
|
+
{ value: 'option2', text: 'Option 2' },
|
|
545
|
+
{ value: 'option3', text: 'Option 3' },
|
|
546
|
+
{ value: 'option4', text: 'Option 4' }
|
|
547
|
+
],
|
|
548
|
+
events: {
|
|
549
|
+
afterChange: updateCallback
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
await nextTick()
|
|
555
|
+
|
|
556
|
+
// Verify initial selection from parent's computed value
|
|
557
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
558
|
+
let selected = slim.getSelected()
|
|
559
|
+
expect(selected).toEqual(['option1', 'option2'])
|
|
560
|
+
|
|
561
|
+
// User makes a change in SlimSelect
|
|
562
|
+
slim.setSelected(['option2', 'option3', 'option4'])
|
|
563
|
+
await nextTick()
|
|
564
|
+
|
|
565
|
+
// Verify the change callback was called
|
|
566
|
+
expect(updateCallback).toHaveBeenCalled()
|
|
567
|
+
|
|
568
|
+
// Verify v-model emitted the update
|
|
569
|
+
const emitted = wrapper.emitted('update:modelValue')
|
|
570
|
+
expect(emitted).toBeTruthy()
|
|
571
|
+
expect(emitted?.[emitted.length - 1]).toEqual([['option2', 'option3', 'option4']])
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
test('updates when parent changes computed data based on new modelValue', async () => {
|
|
575
|
+
// Start with initial values
|
|
576
|
+
const fieldValues = [
|
|
577
|
+
{ value: 'opt1', text: 'Option 1' },
|
|
578
|
+
{ value: 'opt2', text: 'Option 2' },
|
|
579
|
+
{ value: 'opt3', text: 'Option 3' }
|
|
580
|
+
]
|
|
581
|
+
|
|
582
|
+
wrapper = mount(SlimSelectVue, {
|
|
583
|
+
props: {
|
|
584
|
+
modelValue: ['opt1'],
|
|
585
|
+
multiple: true,
|
|
586
|
+
data: fieldValues.map((v) => ({
|
|
587
|
+
value: v.value,
|
|
588
|
+
text: v.text,
|
|
589
|
+
selected: ['opt1'].includes(v.value)
|
|
590
|
+
}))
|
|
591
|
+
}
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
await nextTick()
|
|
595
|
+
let slim = (wrapper.vm as any).slim as SlimSelect
|
|
596
|
+
let selected = slim.getSelected()
|
|
597
|
+
expect(selected).toEqual(['opt1'])
|
|
598
|
+
|
|
599
|
+
// Parent updates its modelValue (e.g., from API or another form field)
|
|
600
|
+
// This should update the computed values and SlimSelect
|
|
601
|
+
await wrapper.setProps({
|
|
602
|
+
modelValue: ['opt2', 'opt3'],
|
|
603
|
+
data: fieldValues.map((v) => ({
|
|
604
|
+
value: v.value,
|
|
605
|
+
text: v.text,
|
|
606
|
+
selected: ['opt2', 'opt3'].includes(v.value)
|
|
607
|
+
}))
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
await nextTick()
|
|
611
|
+
|
|
612
|
+
// Verify SlimSelect reflects the new selection
|
|
613
|
+
slim = (wrapper.vm as any).slim as SlimSelect
|
|
614
|
+
selected = slim.getSelected()
|
|
615
|
+
expect(selected).toEqual(['opt2', 'opt3'])
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
test('handles afterChange callback that triggers parent valueChange method', async () => {
|
|
619
|
+
// Simulates the pattern: user changes SlimSelect -> afterChange -> parent's valueChange() -> emit
|
|
620
|
+
let parentValues = {
|
|
621
|
+
customField1: ['value1']
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const valueChangeCallback = vi.fn(() => {
|
|
625
|
+
// Simulate parent's valueChange method that formats and emits
|
|
626
|
+
const formatted = { customField1: parentValues.customField1 }
|
|
627
|
+
return formatted
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
wrapper = mount(SlimSelectVue, {
|
|
631
|
+
props: {
|
|
632
|
+
modelValue: parentValues.customField1,
|
|
633
|
+
multiple: true,
|
|
634
|
+
data: [
|
|
635
|
+
{ value: 'value1', text: 'Value 1' },
|
|
636
|
+
{ value: 'value2', text: 'Value 2' },
|
|
637
|
+
{ value: 'value3', text: 'Value 3' }
|
|
638
|
+
],
|
|
639
|
+
events: {
|
|
640
|
+
afterChange: (newVal: Option[]) => {
|
|
641
|
+
// Update parent's local values
|
|
642
|
+
parentValues.customField1 = newVal.map((o: Option) => o.value)
|
|
643
|
+
// Call parent's valueChange method
|
|
644
|
+
valueChangeCallback()
|
|
645
|
+
}
|
|
646
|
+
} as Events
|
|
647
|
+
}
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
await nextTick()
|
|
651
|
+
|
|
652
|
+
// User changes selection
|
|
653
|
+
const slim = (wrapper.vm as any).slim as SlimSelect
|
|
654
|
+
slim.setSelected(['value2', 'value3'])
|
|
655
|
+
await nextTick()
|
|
656
|
+
|
|
657
|
+
// Verify the callback chain was triggered
|
|
658
|
+
expect(valueChangeCallback).toHaveBeenCalled()
|
|
659
|
+
expect(parentValues.customField1).toEqual(['value2', 'value3'])
|
|
660
|
+
|
|
661
|
+
// Verify v-model emission
|
|
662
|
+
const emitted = wrapper.emitted('update:modelValue')
|
|
663
|
+
expect(emitted).toBeTruthy()
|
|
664
|
+
expect(emitted?.[emitted.length - 1]).toEqual([['value2', 'value3']])
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
test('maintains reactivity through multiple prop updates from parent computed', async () => {
|
|
668
|
+
// Test that reactivity works through multiple cycles
|
|
669
|
+
wrapper = mount(SlimSelectVue, {
|
|
670
|
+
props: {
|
|
671
|
+
modelValue: ['a'],
|
|
672
|
+
multiple: true,
|
|
673
|
+
data: [
|
|
674
|
+
{ value: 'a', text: 'A' },
|
|
675
|
+
{ value: 'b', text: 'B' },
|
|
676
|
+
{ value: 'c', text: 'C' }
|
|
677
|
+
]
|
|
678
|
+
}
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
await nextTick()
|
|
682
|
+
let slim = (wrapper.vm as any).slim as SlimSelect
|
|
683
|
+
expect(slim.getSelected()).toEqual(['a'])
|
|
684
|
+
|
|
685
|
+
// First parent update
|
|
686
|
+
await wrapper.setProps({ modelValue: ['b'] })
|
|
687
|
+
await nextTick()
|
|
688
|
+
expect(slim.getSelected()).toEqual(['b'])
|
|
689
|
+
|
|
690
|
+
// Second parent update
|
|
691
|
+
await wrapper.setProps({ modelValue: ['a', 'c'] })
|
|
692
|
+
await nextTick()
|
|
693
|
+
expect(slim.getSelected()).toEqual(['a', 'c'])
|
|
694
|
+
|
|
695
|
+
// User makes a change
|
|
696
|
+
slim.setSelected(['b', 'c'])
|
|
697
|
+
await nextTick()
|
|
698
|
+
|
|
699
|
+
// Verify emission
|
|
700
|
+
const emitted = wrapper.emitted('update:modelValue')
|
|
701
|
+
expect(emitted?.[emitted.length - 1]).toEqual([['b', 'c']])
|
|
702
|
+
|
|
703
|
+
// Parent responds to the change
|
|
704
|
+
await wrapper.setProps({ modelValue: ['b', 'c'] })
|
|
705
|
+
await nextTick()
|
|
706
|
+
expect(slim.getSelected()).toEqual(['b', 'c'])
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
test('handles data prop updates from parent computed with selected state', async () => {
|
|
710
|
+
// Simulates when parent recomputes data array with updated selected states
|
|
711
|
+
const allOptions = [
|
|
712
|
+
{ value: 'v1', text: 'Value 1' },
|
|
713
|
+
{ value: 'v2', text: 'Value 2' },
|
|
714
|
+
{ value: 'v3', text: 'Value 3' }
|
|
715
|
+
]
|
|
716
|
+
|
|
717
|
+
const computeDataProp = (selectedValues: string[]) => {
|
|
718
|
+
return allOptions.map((opt) => ({
|
|
719
|
+
value: opt.value,
|
|
720
|
+
text: opt.text,
|
|
721
|
+
selected: selectedValues.includes(opt.value)
|
|
722
|
+
}))
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
wrapper = mount(SlimSelectVue, {
|
|
726
|
+
props: {
|
|
727
|
+
modelValue: ['v1'],
|
|
728
|
+
multiple: true,
|
|
729
|
+
data: computeDataProp(['v1'])
|
|
730
|
+
}
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
await nextTick()
|
|
734
|
+
let slim = (wrapper.vm as any).slim as SlimSelect
|
|
735
|
+
expect(slim.getSelected()).toEqual(['v1'])
|
|
736
|
+
|
|
737
|
+
// Parent's modelValue changes, triggering recompute of data prop
|
|
738
|
+
await wrapper.setProps({
|
|
739
|
+
modelValue: ['v2', 'v3'],
|
|
740
|
+
data: computeDataProp(['v2', 'v3'])
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
await nextTick()
|
|
744
|
+
slim = (wrapper.vm as any).slim as SlimSelect
|
|
745
|
+
expect(slim.getSelected()).toEqual(['v2', 'v3'])
|
|
746
|
+
|
|
747
|
+
// Verify data was updated
|
|
748
|
+
const data = slim.getData()
|
|
749
|
+
expect(data).toHaveLength(3)
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
test('CustomFields use case: parent-child pattern with reactive slot content', async () => {
|
|
753
|
+
// Recreate the actual working pattern: parent passes value to child via v-model
|
|
754
|
+
const ChildComponent = {
|
|
755
|
+
components: { SlimSelectVue },
|
|
756
|
+
template: `
|
|
757
|
+
<SlimSelectVue
|
|
758
|
+
v-model="value"
|
|
759
|
+
multiple
|
|
760
|
+
:events="{ afterChange: () => handleChange() }"
|
|
761
|
+
>
|
|
762
|
+
<option v-for="opt in fieldOptions" :key="opt.value" :value="opt.value">
|
|
763
|
+
{{ opt.name }}
|
|
764
|
+
</option>
|
|
765
|
+
</SlimSelectVue>
|
|
766
|
+
`,
|
|
767
|
+
props: {
|
|
768
|
+
modelValue: { type: Array as PropType<string[]>, required: true },
|
|
769
|
+
fieldOptions: { type: Array, required: true }
|
|
770
|
+
},
|
|
771
|
+
emits: ['update:modelValue'],
|
|
772
|
+
computed: {
|
|
773
|
+
value: {
|
|
774
|
+
get(this: any): string[] {
|
|
775
|
+
return this.modelValue || []
|
|
776
|
+
},
|
|
777
|
+
set(this: any, newValue: string[]) {
|
|
778
|
+
this.$emit('update:modelValue', newValue)
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
methods: {
|
|
783
|
+
handleChange() {
|
|
784
|
+
// Handle selection change
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const ParentComponent = {
|
|
790
|
+
components: { ChildComponent },
|
|
791
|
+
template: `
|
|
792
|
+
<ChildComponent
|
|
793
|
+
v-model="selectedValues"
|
|
794
|
+
:field-options="options"
|
|
795
|
+
/>
|
|
796
|
+
`,
|
|
797
|
+
data() {
|
|
798
|
+
return {
|
|
799
|
+
selectedValues: ['value1'] as string[],
|
|
800
|
+
options: [
|
|
801
|
+
{ value: 'value1', name: 'Value 1' },
|
|
802
|
+
{ value: 'value2', name: 'Value 2' },
|
|
803
|
+
{ value: 'value3', name: 'Value 3' }
|
|
804
|
+
]
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const wrapper = mount(ParentComponent)
|
|
810
|
+
await nextTick()
|
|
811
|
+
|
|
812
|
+
// Verify initial state
|
|
813
|
+
const slimComponent = wrapper.findComponent(SlimSelectVue)
|
|
814
|
+
const slim = (slimComponent.vm as any).slim as SlimSelect
|
|
815
|
+
expect(slim.getSelected()).toEqual(['value1'])
|
|
816
|
+
|
|
817
|
+
// User changes selection via SlimSelect
|
|
818
|
+
slim.setSelected(['value2', 'value3'])
|
|
819
|
+
await nextTick()
|
|
820
|
+
|
|
821
|
+
// Verify parent data updated
|
|
822
|
+
expect((wrapper.vm as any).selectedValues).toEqual(['value2', 'value3'])
|
|
823
|
+
|
|
824
|
+
// Parent's data changes (from API or other form)
|
|
825
|
+
;((wrapper.vm as any).selectedValues as string[]) = ['value1', 'value3']
|
|
826
|
+
await nextTick()
|
|
827
|
+
|
|
828
|
+
// Verify SlimSelect reflects the new selection
|
|
829
|
+
expect(slim.getSelected()).toEqual(['value1', 'value3'])
|
|
830
|
+
})
|
|
831
|
+
})
|
|
832
|
+
})
|