slim-select 3.4.2 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/classes.d.ts +46 -0
- package/dist/helpers.d.ts +5 -0
- package/dist/index.d.ts +53 -318
- package/dist/index.umd.d.ts +2 -0
- package/dist/react/index.d.ts +4 -318
- package/dist/react/index.js +1459 -1696
- package/dist/react/index.umd.js +2 -2
- package/dist/react/react.d.ts +17 -0
- package/dist/react/react.test.d.ts +1 -0
- package/dist/react/slim-select/react/index.d.ts +4 -0
- package/dist/render.d.ts +88 -0
- package/dist/select.d.ts +38 -0
- package/dist/settings.d.ts +39 -0
- package/dist/slimselect.cjs.js +2 -2
- package/dist/slimselect.cjs.js.map +1 -1
- package/dist/slimselect.css +1 -1
- package/dist/slimselect.css.map +1 -1
- package/dist/slimselect.es.js +1233 -1384
- package/dist/slimselect.es.js.map +1 -1
- package/dist/slimselect.iife.js +2 -2
- package/dist/slimselect.iife.js.map +1 -1
- package/dist/slimselect.js +2 -2
- package/dist/slimselect.js.map +1 -1
- package/dist/src/docs/main.d.ts +0 -0
- package/dist/src/docs/router.d.ts +2 -0
- package/dist/src/docs/store.d.ts +12 -0
- package/dist/store.d.ts +53 -0
- package/dist/vue/index.d.ts +4 -318
- package/dist/vue/index.js +1335 -1503
- package/dist/vue/index.umd.js +1 -1
- package/dist/vue/slim-select/vue/index.d.ts +4 -0
- package/package.json +30 -36
- package/src/slim-select/slimselect.scss +34 -5
- package/.nvmrc +0 -1
- package/.prettierignore +0 -1
- package/.prettierrc +0 -7
- package/CODE_OF_CONDUCT.md +0 -46
- package/CONTRIBUTING.md +0 -1
- package/cdn.html +0 -21
- package/docs.go +0 -12
- package/example.html +0 -25
- package/index.html +0 -76
- package/release.js +0 -191
- package/sitemap.js +0 -108
- package/src/slim-select/accessibility.test.ts +0 -782
- package/src/slim-select/classes.test.ts +0 -73
- package/src/slim-select/classes.ts +0 -117
- package/src/slim-select/helpers.test.ts +0 -162
- package/src/slim-select/helpers.ts +0 -61
- package/src/slim-select/index.test.ts +0 -1356
- package/src/slim-select/index.ts +0 -594
- package/src/slim-select/index.umd.ts +0 -8
- package/src/slim-select/react/index.tsx +0 -9
- package/src/slim-select/react/react.test.tsx +0 -498
- package/src/slim-select/react/react.tsx +0 -209
- package/src/slim-select/render.test.ts +0 -1877
- package/src/slim-select/render.ts +0 -1765
- package/src/slim-select/select.test.ts +0 -683
- package/src/slim-select/select.ts +0 -659
- package/src/slim-select/settings.test.ts +0 -285
- package/src/slim-select/settings.ts +0 -78
- package/src/slim-select/store.test.ts +0 -1007
- package/src/slim-select/store.ts +0 -433
- package/src/slim-select/vue/index.ts +0 -9
- package/src/slim-select/vue/vue.test.ts +0 -1091
- package/src/slim-select/vue/vue.vue +0 -224
- package/tsconfig.json +0 -26
- package/vite.config.docs.mts +0 -29
- package/vite.config.lib.es-cjs.ts +0 -40
- package/vite.config.lib.umd-iife.ts +0 -34
- package/vite.config.react.mts +0 -41
- package/vite.config.vue.mts +0 -40
- package/vitest.config.ts +0 -30
|
@@ -1,1356 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment jsdom
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
'use strict'
|
|
6
|
-
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
8
|
-
import SlimSelect from '@/slim-select'
|
|
9
|
-
|
|
10
|
-
describe('SlimSelect Module', () => {
|
|
11
|
-
let slim: SlimSelect
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
document.body.innerHTML = '<select id="test"></select>'
|
|
15
|
-
|
|
16
|
-
slim = new SlimSelect({
|
|
17
|
-
select: '#test'
|
|
18
|
-
})
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
if (slim) {
|
|
23
|
-
slim.destroy()
|
|
24
|
-
}
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe('constructor', () => {
|
|
28
|
-
test('missing select element throws error', () => {
|
|
29
|
-
const errorMock = vi.fn()
|
|
30
|
-
const slim = new SlimSelect({ select: '#invalid', events: { error: errorMock } })
|
|
31
|
-
|
|
32
|
-
expect(slim.select).toBeUndefined()
|
|
33
|
-
expect(slim.store).toBeUndefined()
|
|
34
|
-
expect(slim.render).toBeUndefined()
|
|
35
|
-
expect(errorMock).toHaveBeenCalled()
|
|
36
|
-
expect(errorMock.mock.calls[0][0].message).toBe('Could not find select element')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('invalid element throws error', () => {
|
|
40
|
-
document.body.innerHTML = '<div id="invalid"></div>'
|
|
41
|
-
|
|
42
|
-
const errorMock = vi.fn()
|
|
43
|
-
const slim = new SlimSelect({ select: '#invalid', events: { error: errorMock } })
|
|
44
|
-
|
|
45
|
-
expect(slim.select).toBeUndefined()
|
|
46
|
-
expect(slim.store).toBeUndefined()
|
|
47
|
-
expect(slim.render).toBeUndefined()
|
|
48
|
-
expect(errorMock).toHaveBeenCalled()
|
|
49
|
-
expect(errorMock.mock.calls[0][0].message).toBe('Element isnt of type select')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
test('valid minimal constructor with query string', () => {
|
|
53
|
-
document.body.innerHTML = '<select id="test"></select>'
|
|
54
|
-
|
|
55
|
-
const slimSelect = new SlimSelect({
|
|
56
|
-
select: '#test'
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
expect(slimSelect).toBeInstanceOf(SlimSelect)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('valid minimal constructor with HTML element', () => {
|
|
63
|
-
document.body.innerHTML = '<select id="test"></select>'
|
|
64
|
-
|
|
65
|
-
const slimSelect = new SlimSelect({
|
|
66
|
-
select: document.getElementById('test') as Element
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
expect(slimSelect).toBeInstanceOf(SlimSelect)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('disabled gets applied correctly', () => {
|
|
73
|
-
document.body.innerHTML = '<select id="test"></select>'
|
|
74
|
-
|
|
75
|
-
const slim = new SlimSelect({
|
|
76
|
-
select: document.getElementById('test') as Element,
|
|
77
|
-
settings: {
|
|
78
|
-
disabled: true
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
expect(slim.settings.disabled).toBe(true)
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('enable', () => {
|
|
87
|
-
slim.settings.disabled = true
|
|
88
|
-
|
|
89
|
-
const selectEnableMock = vi.fn()
|
|
90
|
-
const renderEnableMock = vi.fn()
|
|
91
|
-
|
|
92
|
-
slim.select.enable = selectEnableMock
|
|
93
|
-
slim.render.enable = renderEnableMock
|
|
94
|
-
|
|
95
|
-
slim.enable()
|
|
96
|
-
|
|
97
|
-
expect(slim.settings.disabled).toBe(false)
|
|
98
|
-
expect(selectEnableMock).toHaveBeenCalled()
|
|
99
|
-
expect(renderEnableMock).toHaveBeenCalled()
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test('disable', () => {
|
|
103
|
-
slim.settings.disabled = false
|
|
104
|
-
|
|
105
|
-
const selectDisableMock = vi.fn()
|
|
106
|
-
const renderDisableMock = vi.fn()
|
|
107
|
-
|
|
108
|
-
slim.select.disable = selectDisableMock
|
|
109
|
-
slim.render.disable = renderDisableMock
|
|
110
|
-
|
|
111
|
-
slim.disable()
|
|
112
|
-
|
|
113
|
-
expect(slim.settings.disabled).toBe(true)
|
|
114
|
-
expect(selectDisableMock).toHaveBeenCalled()
|
|
115
|
-
expect(renderDisableMock).toHaveBeenCalled()
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
test('multiple - do not render deselect all with no selected options', () => {
|
|
119
|
-
document.body.innerHTML = `<select id="test" multiple>
|
|
120
|
-
<option data-placeholder="true"></option>
|
|
121
|
-
<option id="1" value="1">One</option>
|
|
122
|
-
<option id="2" value="2">Two</option>
|
|
123
|
-
<option id="3" value="3">Two</option>
|
|
124
|
-
</select>`
|
|
125
|
-
|
|
126
|
-
const options = [
|
|
127
|
-
{
|
|
128
|
-
id: '1',
|
|
129
|
-
value: '1',
|
|
130
|
-
text: 'One'
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
id: '2',
|
|
134
|
-
value: '2',
|
|
135
|
-
text: 'Two'
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
id: '3',
|
|
139
|
-
value: '3',
|
|
140
|
-
text: 'Three'
|
|
141
|
-
}
|
|
142
|
-
]
|
|
143
|
-
const config = {
|
|
144
|
-
select: '#test',
|
|
145
|
-
settings: {
|
|
146
|
-
allowDeselect: true
|
|
147
|
-
},
|
|
148
|
-
data: options
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
let slimSelect = new SlimSelect(config)
|
|
152
|
-
expect(slimSelect.store.getSelectType()).toEqual('multiple')
|
|
153
|
-
expect(slimSelect.getSelected()).toHaveLength(0)
|
|
154
|
-
expect(slimSelect.render.main.deselect.main.classList).toContain(slimSelect.render.classes.hide)
|
|
155
|
-
})
|
|
156
|
-
test('multiple - render deselect all option with selected options', () => {
|
|
157
|
-
document.body.innerHTML = `<select id="test" multiple>
|
|
158
|
-
<option data-placeholder="true"></option>
|
|
159
|
-
<option id="1" value="1">One</option>
|
|
160
|
-
<option id="2" value="2">Two</option>
|
|
161
|
-
<option id="3" value="3">Two</option>
|
|
162
|
-
</select>`
|
|
163
|
-
|
|
164
|
-
const options = [
|
|
165
|
-
{
|
|
166
|
-
id: '1',
|
|
167
|
-
value: '1',
|
|
168
|
-
text: 'One',
|
|
169
|
-
selected: true
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
id: '2',
|
|
173
|
-
value: '2',
|
|
174
|
-
text: 'Two',
|
|
175
|
-
selected: true
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
id: '3',
|
|
179
|
-
value: '3',
|
|
180
|
-
text: 'Three'
|
|
181
|
-
}
|
|
182
|
-
]
|
|
183
|
-
const config = {
|
|
184
|
-
select: '#test',
|
|
185
|
-
settings: {
|
|
186
|
-
allowDeselect: true
|
|
187
|
-
},
|
|
188
|
-
data: options
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
let slimSelect = new SlimSelect(config)
|
|
192
|
-
expect(slimSelect.store.getSelectType()).toEqual('multiple')
|
|
193
|
-
expect(slimSelect.getSelected()).toHaveLength(2)
|
|
194
|
-
expect(slimSelect.render.main.deselect.main.classList).not.toContain(slimSelect.render.classes.hide)
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
describe('required attribute support', () => {
|
|
198
|
-
test('select with required attribute remains focusable for validation', () => {
|
|
199
|
-
document.body.innerHTML = '<select id="test" required></select>'
|
|
200
|
-
|
|
201
|
-
const slim = new SlimSelect({
|
|
202
|
-
select: '#test'
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
const selectEl = document.getElementById('test') as HTMLSelectElement
|
|
206
|
-
|
|
207
|
-
// Select should be hidden but still focusable (1px for validation popup)
|
|
208
|
-
expect(selectEl.style.position).toBe('absolute')
|
|
209
|
-
expect(selectEl.style.width).toBe('1px')
|
|
210
|
-
expect(selectEl.style.height).toBe('1px')
|
|
211
|
-
expect(selectEl.style.opacity).toBe('0')
|
|
212
|
-
expect(selectEl.style.margin).toBe('0px')
|
|
213
|
-
expect(selectEl.style.padding).toBe('0px')
|
|
214
|
-
// clip property is deprecated and may not be accessible in all test environments
|
|
215
|
-
// The code sets it, but some browsers/test environments may ignore or clear it
|
|
216
|
-
// Check if clip is set OR if the other hiding properties are sufficient
|
|
217
|
-
const clipValue = selectEl.style.clip
|
|
218
|
-
if (clipValue) {
|
|
219
|
-
expect(clipValue).toContain('rect')
|
|
220
|
-
}
|
|
221
|
-
// If clip is empty, the other hiding properties (position, width, height, opacity) are sufficient
|
|
222
|
-
|
|
223
|
-
// Required attribute should still be present
|
|
224
|
-
expect(selectEl.hasAttribute('required')).toBe(true)
|
|
225
|
-
|
|
226
|
-
// Select should be able to receive focus programmatically
|
|
227
|
-
selectEl.focus()
|
|
228
|
-
expect(document.activeElement).toBe(selectEl)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
test('form validation works with required select', () => {
|
|
232
|
-
// Create a form with required select
|
|
233
|
-
document.body.innerHTML = `
|
|
234
|
-
<form id="test-form">
|
|
235
|
-
<select id="test" name="test-select" required>
|
|
236
|
-
<option value="">Select an option</option>
|
|
237
|
-
<option value="1">Option 1</option>
|
|
238
|
-
<option value="2">Option 2</option>
|
|
239
|
-
</select>
|
|
240
|
-
<button type="submit">Submit</button>
|
|
241
|
-
</form>
|
|
242
|
-
`
|
|
243
|
-
|
|
244
|
-
const slim = new SlimSelect({
|
|
245
|
-
select: '#test'
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
const form = document.getElementById('test-form') as HTMLFormElement
|
|
249
|
-
const selectEl = document.getElementById('test') as HTMLSelectElement
|
|
250
|
-
|
|
251
|
-
// Form should be invalid when nothing is selected
|
|
252
|
-
expect(selectEl.required).toBe(true)
|
|
253
|
-
expect(selectEl.value).toBe('')
|
|
254
|
-
expect(form.checkValidity()).toBe(false)
|
|
255
|
-
|
|
256
|
-
// Select an option
|
|
257
|
-
slim.setSelected(['1'])
|
|
258
|
-
|
|
259
|
-
// Form should now be valid
|
|
260
|
-
expect(selectEl.value).toBe('1')
|
|
261
|
-
expect(form.checkValidity()).toBe(true)
|
|
262
|
-
})
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
describe('keepOrder setting', () => {
|
|
266
|
-
test('keepOrder: false returns values in DOM order (default)', () => {
|
|
267
|
-
document.body.innerHTML = `
|
|
268
|
-
<select id="test" multiple>
|
|
269
|
-
<option value="apple">Apple</option>
|
|
270
|
-
<option value="banana">Banana</option>
|
|
271
|
-
<option value="cherry">Cherry</option>
|
|
272
|
-
</select>
|
|
273
|
-
`
|
|
274
|
-
|
|
275
|
-
const slim = new SlimSelect({
|
|
276
|
-
select: '#test',
|
|
277
|
-
settings: {
|
|
278
|
-
keepOrder: false // DOM order (default)
|
|
279
|
-
}
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
// Select in reverse order: Cherry -> Apple -> Banana
|
|
283
|
-
slim.setSelected(['cherry', 'apple', 'banana'])
|
|
284
|
-
|
|
285
|
-
// Should return in DOM order (how they appear in HTML)
|
|
286
|
-
expect(slim.getSelected()).toEqual(['apple', 'banana', 'cherry'])
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
test('keepOrder: true returns values in selection order', () => {
|
|
290
|
-
document.body.innerHTML = `
|
|
291
|
-
<select id="test" multiple>
|
|
292
|
-
<option value="apple">Apple</option>
|
|
293
|
-
<option value="banana">Banana</option>
|
|
294
|
-
<option value="cherry">Cherry</option>
|
|
295
|
-
</select>
|
|
296
|
-
`
|
|
297
|
-
|
|
298
|
-
const slim = new SlimSelect({
|
|
299
|
-
select: '#test',
|
|
300
|
-
settings: {
|
|
301
|
-
keepOrder: true // Selection order
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
// Select in specific order: Cherry -> Apple -> Banana
|
|
306
|
-
slim.setSelected(['cherry', 'apple', 'banana'])
|
|
307
|
-
|
|
308
|
-
// Should return in the order they were selected (click order)
|
|
309
|
-
expect(slim.getSelected()).toEqual(['cherry', 'apple', 'banana'])
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
test('keepOrder: true preserves click order in getSelected', () => {
|
|
313
|
-
document.body.innerHTML = `
|
|
314
|
-
<select id="test" multiple>
|
|
315
|
-
<option value="1">Option 1</option>
|
|
316
|
-
<option value="2">Option 2</option>
|
|
317
|
-
<option value="3">Option 3</option>
|
|
318
|
-
<option value="4">Option 4</option>
|
|
319
|
-
<option value="5">Option 5</option>
|
|
320
|
-
</select>
|
|
321
|
-
`
|
|
322
|
-
|
|
323
|
-
const slim = new SlimSelect({
|
|
324
|
-
select: '#test',
|
|
325
|
-
settings: {
|
|
326
|
-
keepOrder: true
|
|
327
|
-
}
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
// Simulate user clicking options in random order
|
|
331
|
-
slim.setSelected(['5', '2', '4', '1'])
|
|
332
|
-
|
|
333
|
-
// Should maintain that exact order
|
|
334
|
-
expect(slim.getSelected()).toEqual(['5', '2', '4', '1'])
|
|
335
|
-
})
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
describe('maxValuesShown with deselection', () => {
|
|
339
|
-
test('can still deselect options via dropdown when maxValuesShown is exceeded', () => {
|
|
340
|
-
document.body.innerHTML = `
|
|
341
|
-
<select id="test" multiple>
|
|
342
|
-
<option value="1">Option 1</option>
|
|
343
|
-
<option value="2">Option 2</option>
|
|
344
|
-
<option value="3">Option 3</option>
|
|
345
|
-
<option value="4">Option 4</option>
|
|
346
|
-
<option value="5">Option 5</option>
|
|
347
|
-
</select>
|
|
348
|
-
`
|
|
349
|
-
|
|
350
|
-
const slim = new SlimSelect({
|
|
351
|
-
select: '#test',
|
|
352
|
-
settings: {
|
|
353
|
-
maxValuesShown: 2 // Show "X selected" when more than 2
|
|
354
|
-
}
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
// Select 3 options (exceeds maxValuesShown)
|
|
358
|
-
slim.setSelected(['1', '2', '3'])
|
|
359
|
-
expect(slim.getSelected()).toEqual(['1', '2', '3'])
|
|
360
|
-
|
|
361
|
-
// Verify counter is shown (not individual values)
|
|
362
|
-
const counter = document.querySelector('.ss-max')
|
|
363
|
-
expect(counter?.textContent).toBe('3 selected')
|
|
364
|
-
|
|
365
|
-
// Open dropdown
|
|
366
|
-
slim.open()
|
|
367
|
-
|
|
368
|
-
// Find and click on a selected option to deselect it
|
|
369
|
-
const options = document.querySelectorAll('[role="option"]')
|
|
370
|
-
const selectedOption = Array.from(options).find(
|
|
371
|
-
(opt) => opt.getAttribute('aria-selected') === 'true' && opt.textContent?.includes('Option 1')
|
|
372
|
-
) as HTMLElement
|
|
373
|
-
|
|
374
|
-
expect(selectedOption).toBeTruthy()
|
|
375
|
-
|
|
376
|
-
// Click the selected option - should deselect it
|
|
377
|
-
selectedOption.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
378
|
-
|
|
379
|
-
// Should now only have 2 selected
|
|
380
|
-
expect(slim.getSelected()).toEqual(['2', '3'])
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
test('deselecting via dropdown updates maxValuesShown display', () => {
|
|
384
|
-
document.body.innerHTML = `
|
|
385
|
-
<select id="test" multiple>
|
|
386
|
-
<option value="1">Value 1</option>
|
|
387
|
-
<option value="2">Value 2</option>
|
|
388
|
-
<option value="3">Value 3</option>
|
|
389
|
-
</select>
|
|
390
|
-
`
|
|
391
|
-
|
|
392
|
-
const slim = new SlimSelect({
|
|
393
|
-
select: '#test',
|
|
394
|
-
settings: {
|
|
395
|
-
maxValuesShown: 2
|
|
396
|
-
}
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
// Select 3 options (shows "3 selected")
|
|
400
|
-
slim.setSelected(['1', '2', '3'])
|
|
401
|
-
|
|
402
|
-
// Open and deselect one
|
|
403
|
-
slim.open()
|
|
404
|
-
const options = document.querySelectorAll('[role="option"]')
|
|
405
|
-
const firstSelected = Array.from(options).find(
|
|
406
|
-
(opt) => opt.getAttribute('aria-selected') === 'true'
|
|
407
|
-
) as HTMLElement
|
|
408
|
-
firstSelected.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
409
|
-
|
|
410
|
-
// Now only 2 selected - should show individual values again (not counter)
|
|
411
|
-
const individualValues = document.querySelectorAll('.ss-value')
|
|
412
|
-
expect(individualValues.length).toBeGreaterThan(0)
|
|
413
|
-
|
|
414
|
-
const counter = document.querySelector('.ss-max')
|
|
415
|
-
expect(counter).toBeNull()
|
|
416
|
-
})
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
describe('Search State Regression Tests', () => {
|
|
420
|
-
let slim: SlimSelect
|
|
421
|
-
let searchMock: (searchValue: string, currentData: any[]) => any[]
|
|
422
|
-
|
|
423
|
-
beforeEach(() => {
|
|
424
|
-
document.body.innerHTML = '<select id="searchTest"></select>'
|
|
425
|
-
|
|
426
|
-
searchMock = vi.fn().mockImplementation((searchValue: string, currentData: any[]) => {
|
|
427
|
-
// Mock search results based on search value
|
|
428
|
-
if (searchValue.length >= 2) {
|
|
429
|
-
return [
|
|
430
|
-
{ value: 'null', text: 'Null' },
|
|
431
|
-
{ value: 'eins', text: 'Eins' },
|
|
432
|
-
{ value: 'zwei', text: 'Zwei' },
|
|
433
|
-
{ value: 'drei', text: 'Drei' },
|
|
434
|
-
{ value: 'vier', text: 'Vier' },
|
|
435
|
-
{ value: 'funf', text: 'Fünf' },
|
|
436
|
-
{ value: 'sechs', text: 'Sechs' },
|
|
437
|
-
{ value: 'sieben', text: 'Sieben' },
|
|
438
|
-
{ value: 'acht', text: 'Acht' },
|
|
439
|
-
{ value: 'neun', text: 'Neun' },
|
|
440
|
-
{ value: 'zehn', text: 'Zehn' }
|
|
441
|
-
]
|
|
442
|
-
}
|
|
443
|
-
return []
|
|
444
|
-
}) as typeof searchMock
|
|
445
|
-
|
|
446
|
-
slim = new SlimSelect({
|
|
447
|
-
select: '#searchTest',
|
|
448
|
-
data: [
|
|
449
|
-
{ value: 'a', text: 'A' },
|
|
450
|
-
{ value: 'b', text: 'B' }
|
|
451
|
-
],
|
|
452
|
-
events: {
|
|
453
|
-
search: searchMock
|
|
454
|
-
}
|
|
455
|
-
})
|
|
456
|
-
})
|
|
457
|
-
|
|
458
|
-
afterEach(() => {
|
|
459
|
-
// Clean up to avoid timeout errors
|
|
460
|
-
if (slim) {
|
|
461
|
-
slim.destroy()
|
|
462
|
-
}
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
test('should maintain search results when reopening dropdown', () => {
|
|
466
|
-
// 1. Open dropdown - should show static options [A, B]
|
|
467
|
-
slim.open()
|
|
468
|
-
let options = document.querySelectorAll('.ss-option')
|
|
469
|
-
expect(options).toHaveLength(2)
|
|
470
|
-
expect(options[0].textContent).toBe('A')
|
|
471
|
-
expect(options[1].textContent).toBe('B')
|
|
472
|
-
slim.close()
|
|
473
|
-
|
|
474
|
-
// 2. Search - should update store data with search results
|
|
475
|
-
slim.search('te')
|
|
476
|
-
options = document.querySelectorAll('.ss-option')
|
|
477
|
-
// Should have search results (11) - static options are replaced
|
|
478
|
-
expect(options.length).toBeGreaterThanOrEqual(11)
|
|
479
|
-
|
|
480
|
-
// 3. Close and reopen - search results persist in store
|
|
481
|
-
slim.close()
|
|
482
|
-
slim.open()
|
|
483
|
-
options = document.querySelectorAll('.ss-option')
|
|
484
|
-
// Store data should still contain search results
|
|
485
|
-
expect(options.length).toBeGreaterThanOrEqual(11)
|
|
486
|
-
})
|
|
487
|
-
|
|
488
|
-
test('should clear search input when closing dropdown', async () => {
|
|
489
|
-
// Open dropdown first
|
|
490
|
-
slim.open()
|
|
491
|
-
|
|
492
|
-
// Search for something
|
|
493
|
-
slim.search('te')
|
|
494
|
-
expect(slim.render.content.search.input.value).toBe('te')
|
|
495
|
-
|
|
496
|
-
// Close dropdown - this clears the search input
|
|
497
|
-
slim.close()
|
|
498
|
-
|
|
499
|
-
// Wait for close to complete (timeoutDelay is 200ms by default)
|
|
500
|
-
await new Promise((resolve) => setTimeout(resolve, 300))
|
|
501
|
-
|
|
502
|
-
// Search input should be cleared
|
|
503
|
-
expect(slim.render.content.search.input.value).toBe('')
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
test('should keep search input when keepSearch is true', async () => {
|
|
507
|
-
// Create new instance with keepSearch enabled
|
|
508
|
-
const select = document.createElement('select')
|
|
509
|
-
select.innerHTML = `
|
|
510
|
-
<option value="a">A</option>
|
|
511
|
-
<option value="b">B</option>
|
|
512
|
-
<option value="c">C</option>
|
|
513
|
-
`
|
|
514
|
-
document.body.appendChild(select)
|
|
515
|
-
|
|
516
|
-
const slimWithKeepSearch = new SlimSelect({
|
|
517
|
-
select: select,
|
|
518
|
-
settings: {
|
|
519
|
-
keepSearch: true
|
|
520
|
-
}
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
// Open dropdown
|
|
524
|
-
slimWithKeepSearch.open()
|
|
525
|
-
|
|
526
|
-
// Search for something
|
|
527
|
-
slimWithKeepSearch.search('test')
|
|
528
|
-
expect(slimWithKeepSearch.render.content.search.input.value).toBe('test')
|
|
529
|
-
|
|
530
|
-
// Close dropdown
|
|
531
|
-
slimWithKeepSearch.close()
|
|
532
|
-
|
|
533
|
-
// Search input should NOT be cleared when keepSearch is true
|
|
534
|
-
expect(slimWithKeepSearch.render.content.search.input.value).toBe('test')
|
|
535
|
-
|
|
536
|
-
// Cleanup
|
|
537
|
-
slimWithKeepSearch.destroy()
|
|
538
|
-
document.body.removeChild(select)
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
test('should persist search results in store across close/open cycles', () => {
|
|
542
|
-
// Search for something
|
|
543
|
-
slim.search('te')
|
|
544
|
-
|
|
545
|
-
// Search results are now in the store
|
|
546
|
-
let options = document.querySelectorAll('.ss-option')
|
|
547
|
-
expect(options.length).toBeGreaterThanOrEqual(11)
|
|
548
|
-
|
|
549
|
-
// Close and reopen - store data persists
|
|
550
|
-
slim.close()
|
|
551
|
-
slim.open()
|
|
552
|
-
options = document.querySelectorAll('.ss-option')
|
|
553
|
-
expect(options.length).toBeGreaterThanOrEqual(11) // Store data maintained
|
|
554
|
-
|
|
555
|
-
slim.close()
|
|
556
|
-
slim.open()
|
|
557
|
-
options = document.querySelectorAll('.ss-option')
|
|
558
|
-
expect(options.length).toBeGreaterThanOrEqual(11) // Still maintained after multiple cycles
|
|
559
|
-
})
|
|
560
|
-
|
|
561
|
-
test('should handle selection from search results correctly', () => {
|
|
562
|
-
// Search for options
|
|
563
|
-
slim.search('te')
|
|
564
|
-
|
|
565
|
-
// Select first search result (should be from search results)
|
|
566
|
-
const options = document.querySelectorAll('.ss-option')
|
|
567
|
-
options[0].dispatchEvent(new MouseEvent('click'))
|
|
568
|
-
|
|
569
|
-
// Check that an option is selected
|
|
570
|
-
const selected = slim.getSelected()
|
|
571
|
-
expect(selected.length).toBe(1)
|
|
572
|
-
|
|
573
|
-
// Close and reopen
|
|
574
|
-
slim.close()
|
|
575
|
-
slim.open()
|
|
576
|
-
|
|
577
|
-
// Should still have search results in store
|
|
578
|
-
const reopenedOptions = document.querySelectorAll('.ss-option')
|
|
579
|
-
expect(reopenedOptions.length).toBeGreaterThanOrEqual(11)
|
|
580
|
-
|
|
581
|
-
// Selected value should be preserved
|
|
582
|
-
expect(slim.getSelected()).toEqual(selected)
|
|
583
|
-
})
|
|
584
|
-
|
|
585
|
-
test('should handle multiple selections from search results', () => {
|
|
586
|
-
// Configure for multi-select
|
|
587
|
-
slim = new SlimSelect({
|
|
588
|
-
select: '#searchTest',
|
|
589
|
-
data: [
|
|
590
|
-
{ value: 'a', text: 'A' },
|
|
591
|
-
{ value: 'b', text: 'B' }
|
|
592
|
-
],
|
|
593
|
-
settings: {
|
|
594
|
-
isMultiple: true
|
|
595
|
-
},
|
|
596
|
-
events: {
|
|
597
|
-
search: searchMock
|
|
598
|
-
}
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
// Search for options
|
|
602
|
-
slim.search('te')
|
|
603
|
-
|
|
604
|
-
// Select first two search results (Null, Eins) using Cmd+Click
|
|
605
|
-
const options = document.querySelectorAll('.ss-option')
|
|
606
|
-
options[0].dispatchEvent(new MouseEvent('click', { metaKey: true })) // Cmd+Click to keep dropdown open
|
|
607
|
-
options[1].dispatchEvent(new MouseEvent('click', { metaKey: true })) // Cmd+Click to keep dropdown open
|
|
608
|
-
|
|
609
|
-
// Note: Search results are temporary and not added to the store
|
|
610
|
-
// So the selection won't persist in the store, but the search state should be maintained
|
|
611
|
-
|
|
612
|
-
// Close and reopen
|
|
613
|
-
slim.close()
|
|
614
|
-
slim.open()
|
|
615
|
-
|
|
616
|
-
// Should still show search results (search state maintained)
|
|
617
|
-
const reopenedOptions = document.querySelectorAll('.ss-option')
|
|
618
|
-
// Multi-select may have additional options like deselect all, so we check for at least 11
|
|
619
|
-
expect(reopenedOptions.length).toBeGreaterThanOrEqual(11)
|
|
620
|
-
|
|
621
|
-
// Search results are maintained but selections from search results don't persist in store
|
|
622
|
-
// This is expected behavior - search results are temporary
|
|
623
|
-
})
|
|
624
|
-
|
|
625
|
-
test('should handle new search after reopening', () => {
|
|
626
|
-
// Initial search
|
|
627
|
-
slim.search('te')
|
|
628
|
-
let options = document.querySelectorAll('.ss-option')
|
|
629
|
-
expect(options.length).toBeGreaterThanOrEqual(11)
|
|
630
|
-
|
|
631
|
-
// Close and reopen
|
|
632
|
-
slim.close()
|
|
633
|
-
slim.open()
|
|
634
|
-
|
|
635
|
-
// Should still have previous search results in store
|
|
636
|
-
options = document.querySelectorAll('.ss-option')
|
|
637
|
-
expect(options.length).toBeGreaterThanOrEqual(11)
|
|
638
|
-
|
|
639
|
-
// Perform new search - this updates the store with new results
|
|
640
|
-
slim.search('ei')
|
|
641
|
-
options = document.querySelectorAll('.ss-option')
|
|
642
|
-
|
|
643
|
-
// Should show new search results
|
|
644
|
-
expect(options.length).toBeGreaterThanOrEqual(11)
|
|
645
|
-
})
|
|
646
|
-
|
|
647
|
-
test('should handle search with no results correctly', () => {
|
|
648
|
-
// Search for something that returns no results
|
|
649
|
-
slim.search('xyz')
|
|
650
|
-
|
|
651
|
-
// Should show no results message (searchText)
|
|
652
|
-
const noResults = document.querySelector('.ss-search')
|
|
653
|
-
expect(noResults).toBeTruthy()
|
|
654
|
-
|
|
655
|
-
// Close and reopen
|
|
656
|
-
slim.close()
|
|
657
|
-
slim.open()
|
|
658
|
-
|
|
659
|
-
// Should still show no results (search state maintained)
|
|
660
|
-
const stillNoResults = document.querySelector('.ss-search')
|
|
661
|
-
expect(stillNoResults).toBeTruthy()
|
|
662
|
-
})
|
|
663
|
-
|
|
664
|
-
test('should handle async search results correctly', async () => {
|
|
665
|
-
// Mock async search
|
|
666
|
-
const asyncSearchMock = vi.fn().mockResolvedValue([
|
|
667
|
-
{ value: 'async1', text: 'Async Result 1' },
|
|
668
|
-
{ value: 'async2', text: 'Async Result 2' }
|
|
669
|
-
])
|
|
670
|
-
|
|
671
|
-
slim = new SlimSelect({
|
|
672
|
-
select: '#searchTest',
|
|
673
|
-
data: [
|
|
674
|
-
{ value: 'a', text: 'A' },
|
|
675
|
-
{ value: 'b', text: 'B' }
|
|
676
|
-
],
|
|
677
|
-
events: {
|
|
678
|
-
search: asyncSearchMock
|
|
679
|
-
}
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
// Trigger async search
|
|
683
|
-
slim.search('async')
|
|
684
|
-
|
|
685
|
-
// Wait for async results
|
|
686
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
687
|
-
|
|
688
|
-
// Check results are displayed (should include the async results)
|
|
689
|
-
let options = document.querySelectorAll('.ss-option')
|
|
690
|
-
expect(options.length).toBeGreaterThanOrEqual(2)
|
|
691
|
-
|
|
692
|
-
// Find the async results in the options
|
|
693
|
-
const asyncOptions = Array.from(options).filter((option) => option.textContent?.includes('Async Result'))
|
|
694
|
-
expect(asyncOptions).toHaveLength(2)
|
|
695
|
-
expect(asyncOptions[0].textContent).toBe('Async Result 1')
|
|
696
|
-
expect(asyncOptions[1].textContent).toBe('Async Result 2')
|
|
697
|
-
|
|
698
|
-
// Close and reopen
|
|
699
|
-
slim.close()
|
|
700
|
-
slim.open()
|
|
701
|
-
|
|
702
|
-
// Should still show async search results
|
|
703
|
-
options = document.querySelectorAll('.ss-option')
|
|
704
|
-
const reopenedAsyncOptions = Array.from(options).filter((option) => option.textContent?.includes('Async Result'))
|
|
705
|
-
expect(reopenedAsyncOptions).toHaveLength(2)
|
|
706
|
-
expect(reopenedAsyncOptions[0].textContent).toBe('Async Result 1')
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
test('issue #671: first search result is selectable when allowDeselect and no prior selection (single select)', async () => {
|
|
710
|
-
document.body.innerHTML = '<select id="searchFirstResult"></select>'
|
|
711
|
-
|
|
712
|
-
const asyncSearchMock = vi.fn().mockResolvedValue([
|
|
713
|
-
{ value: 'first', text: 'First Result' },
|
|
714
|
-
{ value: 'second', text: 'Second Result' }
|
|
715
|
-
])
|
|
716
|
-
|
|
717
|
-
const slimFirst = new SlimSelect({
|
|
718
|
-
select: '#searchFirstResult',
|
|
719
|
-
data: [],
|
|
720
|
-
settings: { allowDeselect: true },
|
|
721
|
-
events: { search: asyncSearchMock }
|
|
722
|
-
})
|
|
723
|
-
|
|
724
|
-
slimFirst.open()
|
|
725
|
-
slimFirst.search('xy')
|
|
726
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
727
|
-
|
|
728
|
-
const options = document.querySelectorAll('.ss-option')
|
|
729
|
-
expect(options.length).toBeGreaterThanOrEqual(2)
|
|
730
|
-
|
|
731
|
-
const firstOption = Array.from(options).find((el) => el.textContent?.trim() === 'First Result')
|
|
732
|
-
expect(firstOption).toBeTruthy()
|
|
733
|
-
expect(firstOption?.getAttribute('aria-selected')).toBe('false')
|
|
734
|
-
|
|
735
|
-
firstOption!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
736
|
-
expect(slimFirst.getSelected()).toEqual(['first'])
|
|
737
|
-
|
|
738
|
-
slimFirst.destroy()
|
|
739
|
-
})
|
|
740
|
-
|
|
741
|
-
test('should update store data with search results', () => {
|
|
742
|
-
// Test that search updates the store
|
|
743
|
-
slim.search('te')
|
|
744
|
-
expect(searchMock).toHaveBeenCalledWith('te', expect.any(Array))
|
|
745
|
-
|
|
746
|
-
let options = document.querySelectorAll('.ss-option')
|
|
747
|
-
expect(options.length).toBeGreaterThanOrEqual(11)
|
|
748
|
-
|
|
749
|
-
// Search results are now in the store
|
|
750
|
-
const storeData = slim.getData()
|
|
751
|
-
expect(storeData.length).toBeGreaterThanOrEqual(11)
|
|
752
|
-
|
|
753
|
-
// Clear search - this now returns all current data in store
|
|
754
|
-
slim.search('')
|
|
755
|
-
options = document.querySelectorAll('.ss-option')
|
|
756
|
-
|
|
757
|
-
// Empty search now shows all current data in store (search results)
|
|
758
|
-
expect(options.length).toBeGreaterThanOrEqual(11)
|
|
759
|
-
})
|
|
760
|
-
})
|
|
761
|
-
|
|
762
|
-
describe('label click support', () => {
|
|
763
|
-
test('clicking label with for attribute opens SlimSelect', async () => {
|
|
764
|
-
document.body.innerHTML = `
|
|
765
|
-
<label for="label-test-select">Select a Country</label>
|
|
766
|
-
<select id="label-test-select">
|
|
767
|
-
<option value="usa">United States</option>
|
|
768
|
-
<option value="uk">United Kingdom</option>
|
|
769
|
-
<option value="canada">Canada</option>
|
|
770
|
-
</select>
|
|
771
|
-
`
|
|
772
|
-
|
|
773
|
-
const slim = new SlimSelect({
|
|
774
|
-
select: '#label-test-select'
|
|
775
|
-
})
|
|
776
|
-
|
|
777
|
-
// Initially closed
|
|
778
|
-
expect(slim.settings.isOpen).toBe(false)
|
|
779
|
-
|
|
780
|
-
// Click the label
|
|
781
|
-
const label = document.querySelector('label[for="label-test-select"]') as HTMLLabelElement
|
|
782
|
-
label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
783
|
-
|
|
784
|
-
// Wait for setTimeout in label handler
|
|
785
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
786
|
-
|
|
787
|
-
// SlimSelect should be open
|
|
788
|
-
expect(slim.settings.isOpen).toBe(true)
|
|
789
|
-
|
|
790
|
-
slim.destroy()
|
|
791
|
-
})
|
|
792
|
-
|
|
793
|
-
test('clicking wrapped label opens SlimSelect', async () => {
|
|
794
|
-
document.body.innerHTML = `
|
|
795
|
-
<label>
|
|
796
|
-
Select a Country
|
|
797
|
-
<select id="wrapped-label-select">
|
|
798
|
-
<option value="usa">United States</option>
|
|
799
|
-
<option value="uk">United Kingdom</option>
|
|
800
|
-
</select>
|
|
801
|
-
</label>
|
|
802
|
-
`
|
|
803
|
-
|
|
804
|
-
const slim = new SlimSelect({
|
|
805
|
-
select: '#wrapped-label-select'
|
|
806
|
-
})
|
|
807
|
-
|
|
808
|
-
// Initially closed
|
|
809
|
-
expect(slim.settings.isOpen).toBe(false)
|
|
810
|
-
|
|
811
|
-
// Click the label
|
|
812
|
-
const label = document.querySelector('label') as HTMLLabelElement
|
|
813
|
-
label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
814
|
-
|
|
815
|
-
// Wait for setTimeout in label handler
|
|
816
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
817
|
-
|
|
818
|
-
// SlimSelect should be open
|
|
819
|
-
expect(slim.settings.isOpen).toBe(true)
|
|
820
|
-
|
|
821
|
-
slim.destroy()
|
|
822
|
-
})
|
|
823
|
-
|
|
824
|
-
test('clicking label does not open SlimSelect when disabled', async () => {
|
|
825
|
-
document.body.innerHTML = `
|
|
826
|
-
<label for="disabled-label-select">Select a Country</label>
|
|
827
|
-
<select id="disabled-label-select">
|
|
828
|
-
<option value="usa">United States</option>
|
|
829
|
-
</select>
|
|
830
|
-
`
|
|
831
|
-
|
|
832
|
-
const slim = new SlimSelect({
|
|
833
|
-
select: '#disabled-label-select',
|
|
834
|
-
settings: {
|
|
835
|
-
disabled: true
|
|
836
|
-
}
|
|
837
|
-
})
|
|
838
|
-
|
|
839
|
-
// Initially closed
|
|
840
|
-
expect(slim.settings.isOpen).toBe(false)
|
|
841
|
-
|
|
842
|
-
// Click the label
|
|
843
|
-
const label = document.querySelector('label[for="disabled-label-select"]') as HTMLLabelElement
|
|
844
|
-
label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
845
|
-
|
|
846
|
-
// Wait for setTimeout in label handler
|
|
847
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
848
|
-
|
|
849
|
-
// SlimSelect should remain closed when disabled
|
|
850
|
-
expect(slim.settings.isOpen).toBe(false)
|
|
851
|
-
|
|
852
|
-
slim.destroy()
|
|
853
|
-
})
|
|
854
|
-
|
|
855
|
-
test('SlimSelect automatically assigns id to select if missing for label association', () => {
|
|
856
|
-
document.body.innerHTML = `
|
|
857
|
-
<label for="auto-id-select">Select a Country</label>
|
|
858
|
-
<select>
|
|
859
|
-
<option value="usa">United States</option>
|
|
860
|
-
</select>
|
|
861
|
-
`
|
|
862
|
-
|
|
863
|
-
// Note: This won't actually work because the label's 'for' points to 'auto-id-select'
|
|
864
|
-
// but we're selecting the select without an id. This test verifies that SlimSelect
|
|
865
|
-
// assigns an id. The actual id will be generated, so the label won't match.
|
|
866
|
-
// Let's test the id assignment instead.
|
|
867
|
-
document.body.innerHTML = '<select><option value="usa">United States</option></select>'
|
|
868
|
-
|
|
869
|
-
const selectElement = document.querySelector('select') as HTMLSelectElement
|
|
870
|
-
const slim = new SlimSelect({
|
|
871
|
-
select: selectElement
|
|
872
|
-
})
|
|
873
|
-
|
|
874
|
-
// SlimSelect should have assigned an id to the select
|
|
875
|
-
expect(selectElement.id).toBeTruthy()
|
|
876
|
-
expect(selectElement.id).toBe(slim.settings.id)
|
|
877
|
-
|
|
878
|
-
slim.destroy()
|
|
879
|
-
})
|
|
880
|
-
|
|
881
|
-
test('label handlers are cleaned up on destroy', async () => {
|
|
882
|
-
document.body.innerHTML = `
|
|
883
|
-
<label for="cleanup-test-select">Select a Country</label>
|
|
884
|
-
<select id="cleanup-test-select">
|
|
885
|
-
<option value="usa">United States</option>
|
|
886
|
-
</select>
|
|
887
|
-
`
|
|
888
|
-
|
|
889
|
-
const slim = new SlimSelect({
|
|
890
|
-
select: '#cleanup-test-select'
|
|
891
|
-
})
|
|
892
|
-
|
|
893
|
-
// Verify label handler works
|
|
894
|
-
const label = document.querySelector('label[for="cleanup-test-select"]') as HTMLLabelElement
|
|
895
|
-
label.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
896
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
897
|
-
expect(slim.settings.isOpen).toBe(true)
|
|
898
|
-
|
|
899
|
-
// Close and destroy
|
|
900
|
-
slim.close()
|
|
901
|
-
slim.destroy()
|
|
902
|
-
|
|
903
|
-
// After destroy, label click should not open SlimSelect
|
|
904
|
-
// (though SlimSelect is destroyed, so we can't check isOpen)
|
|
905
|
-
// But we verify no errors are thrown
|
|
906
|
-
expect(() => {
|
|
907
|
-
label.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
908
|
-
}).not.toThrow()
|
|
909
|
-
})
|
|
910
|
-
|
|
911
|
-
test('clicking main div closes SlimSelect when wrapped in label', async () => {
|
|
912
|
-
document.body.innerHTML = `
|
|
913
|
-
<label>
|
|
914
|
-
Select a Country
|
|
915
|
-
<select id="wrapped-label-close-test">
|
|
916
|
-
<option value="usa">United States</option>
|
|
917
|
-
<option value="uk">United Kingdom</option>
|
|
918
|
-
</select>
|
|
919
|
-
</label>
|
|
920
|
-
`
|
|
921
|
-
|
|
922
|
-
const slim = new SlimSelect({
|
|
923
|
-
select: '#wrapped-label-close-test',
|
|
924
|
-
settings: {
|
|
925
|
-
allowDeselect: true
|
|
926
|
-
}
|
|
927
|
-
})
|
|
928
|
-
|
|
929
|
-
// Initially closed
|
|
930
|
-
expect(slim.settings.isOpen).toBe(false)
|
|
931
|
-
|
|
932
|
-
// Click the label to open
|
|
933
|
-
const label = document.querySelector('label') as HTMLLabelElement
|
|
934
|
-
label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
935
|
-
await new Promise((r) => setTimeout(r, 300)) // after animation
|
|
936
|
-
expect(slim.settings.isOpen).toBe(true)
|
|
937
|
-
|
|
938
|
-
// Now click the main div to close
|
|
939
|
-
const mainDiv = document.querySelector(`.ss-main[data-id="${slim.settings.id}"]`) as HTMLElement
|
|
940
|
-
expect(mainDiv).toBeTruthy()
|
|
941
|
-
|
|
942
|
-
mainDiv.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
943
|
-
await new Promise((r) => setTimeout(r, 300)) // after animation
|
|
944
|
-
|
|
945
|
-
// SlimSelect should be closed
|
|
946
|
-
expect(slim.settings.isOpen).toBe(false)
|
|
947
|
-
|
|
948
|
-
slim.destroy()
|
|
949
|
-
})
|
|
950
|
-
|
|
951
|
-
test('deselect button works when wrapped in label', async () => {
|
|
952
|
-
document.body.innerHTML = `
|
|
953
|
-
<label>
|
|
954
|
-
Select a Country
|
|
955
|
-
<select id="wrapped-label-deselect-test">
|
|
956
|
-
<option value="usa">United States</option>
|
|
957
|
-
<option value="uk">United Kingdom</option>
|
|
958
|
-
</select>
|
|
959
|
-
</label>
|
|
960
|
-
`
|
|
961
|
-
|
|
962
|
-
const slim = new SlimSelect({
|
|
963
|
-
select: '#wrapped-label-deselect-test',
|
|
964
|
-
settings: {
|
|
965
|
-
allowDeselect: true
|
|
966
|
-
}
|
|
967
|
-
})
|
|
968
|
-
|
|
969
|
-
// Select an option
|
|
970
|
-
slim.setSelected('usa')
|
|
971
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
972
|
-
expect(slim.getSelected()).toEqual(['usa'])
|
|
973
|
-
|
|
974
|
-
// Click the deselect button
|
|
975
|
-
const deselectButton = document.querySelector(
|
|
976
|
-
`.ss-main[data-id="${slim.settings.id}"] .ss-deselect`
|
|
977
|
-
) as HTMLElement
|
|
978
|
-
expect(deselectButton).toBeTruthy()
|
|
979
|
-
|
|
980
|
-
deselectButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
981
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
982
|
-
|
|
983
|
-
// Should be deselected (returns array with empty string for single select)
|
|
984
|
-
expect(slim.getSelected()).toEqual([''])
|
|
985
|
-
|
|
986
|
-
slim.destroy()
|
|
987
|
-
})
|
|
988
|
-
|
|
989
|
-
test('clicking main div toggles when wrapped in label', async () => {
|
|
990
|
-
document.body.innerHTML = `
|
|
991
|
-
<label>
|
|
992
|
-
Select a Country
|
|
993
|
-
<select id="wrapped-label-toggle-test">
|
|
994
|
-
<option value="usa">United States</option>
|
|
995
|
-
<option value="uk">United Kingdom</option>
|
|
996
|
-
</select>
|
|
997
|
-
</label>
|
|
998
|
-
`
|
|
999
|
-
|
|
1000
|
-
const slim = new SlimSelect({
|
|
1001
|
-
select: '#wrapped-label-toggle-test'
|
|
1002
|
-
})
|
|
1003
|
-
|
|
1004
|
-
// Initially closed
|
|
1005
|
-
expect(slim.settings.isOpen).toBe(false)
|
|
1006
|
-
|
|
1007
|
-
// Click main div to open
|
|
1008
|
-
const mainDiv = document.querySelector(`.ss-main[data-id="${slim.settings.id}"]`) as HTMLElement
|
|
1009
|
-
mainDiv.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
1010
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
1011
|
-
expect(slim.settings.isOpen).toBe(true)
|
|
1012
|
-
|
|
1013
|
-
// Click main div again to close
|
|
1014
|
-
mainDiv.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
1015
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
1016
|
-
expect(slim.settings.isOpen).toBe(false)
|
|
1017
|
-
|
|
1018
|
-
// Click main div again to open
|
|
1019
|
-
mainDiv.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
1020
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
1021
|
-
expect(slim.settings.isOpen).toBe(true)
|
|
1022
|
-
|
|
1023
|
-
slim.destroy()
|
|
1024
|
-
})
|
|
1025
|
-
|
|
1026
|
-
test('clicking label on second select closes first select when both have labels', async () => {
|
|
1027
|
-
document.body.innerHTML = `
|
|
1028
|
-
<label>
|
|
1029
|
-
First Select
|
|
1030
|
-
<select id="first-select">
|
|
1031
|
-
<option value="1">Option 1</option>
|
|
1032
|
-
<option value="2">Option 2</option>
|
|
1033
|
-
</select>
|
|
1034
|
-
</label>
|
|
1035
|
-
<label>
|
|
1036
|
-
Second Select
|
|
1037
|
-
<select id="second-select">
|
|
1038
|
-
<option value="a">Option A</option>
|
|
1039
|
-
<option value="b">Option B</option>
|
|
1040
|
-
</select>
|
|
1041
|
-
</label>
|
|
1042
|
-
`
|
|
1043
|
-
|
|
1044
|
-
const slim1 = new SlimSelect({
|
|
1045
|
-
select: '#first-select'
|
|
1046
|
-
})
|
|
1047
|
-
|
|
1048
|
-
const slim2 = new SlimSelect({
|
|
1049
|
-
select: '#second-select'
|
|
1050
|
-
})
|
|
1051
|
-
|
|
1052
|
-
// Both should be closed initially
|
|
1053
|
-
expect(slim1.settings.isOpen).toBe(false)
|
|
1054
|
-
expect(slim2.settings.isOpen).toBe(false)
|
|
1055
|
-
|
|
1056
|
-
// Click first label to open first select
|
|
1057
|
-
const label1 = document.querySelector('label:first-of-type') as HTMLLabelElement
|
|
1058
|
-
label1.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
1059
|
-
await new Promise((r) => setTimeout(r, 300))
|
|
1060
|
-
expect(slim1.settings.isOpen).toBe(true)
|
|
1061
|
-
expect(slim2.settings.isOpen).toBe(false)
|
|
1062
|
-
|
|
1063
|
-
// Click second label - should close first and open second
|
|
1064
|
-
const label2 = document.querySelector('label:last-of-type') as HTMLLabelElement
|
|
1065
|
-
label2.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
1066
|
-
await new Promise((r) => setTimeout(r, 300))
|
|
1067
|
-
expect(slim1.settings.isOpen).toBe(false)
|
|
1068
|
-
expect(slim2.settings.isOpen).toBe(true)
|
|
1069
|
-
|
|
1070
|
-
// Click first label again - should close second and open first
|
|
1071
|
-
label1.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
1072
|
-
await new Promise((r) => setTimeout(r, 300))
|
|
1073
|
-
expect(slim1.settings.isOpen).toBe(true)
|
|
1074
|
-
expect(slim2.settings.isOpen).toBe(false)
|
|
1075
|
-
|
|
1076
|
-
slim1.destroy()
|
|
1077
|
-
slim2.destroy()
|
|
1078
|
-
})
|
|
1079
|
-
})
|
|
1080
|
-
|
|
1081
|
-
describe('option changes race condition scenarios', () => {
|
|
1082
|
-
test('options should persist when added to select and value is set immediately', async () => {
|
|
1083
|
-
// Reproduce the issue: empty select, init SlimSelect, add options, set value
|
|
1084
|
-
document.body.innerHTML = '<select id="test-select"></select>'
|
|
1085
|
-
|
|
1086
|
-
const selectElement = document.getElementById('test-select') as HTMLSelectElement
|
|
1087
|
-
const slim = new SlimSelect({
|
|
1088
|
-
select: selectElement
|
|
1089
|
-
})
|
|
1090
|
-
|
|
1091
|
-
// Add option directly to the select element (not through SlimSelect API)
|
|
1092
|
-
const newOption = document.createElement('option')
|
|
1093
|
-
newOption.value = '1'
|
|
1094
|
-
newOption.textContent = 'one'
|
|
1095
|
-
selectElement.appendChild(newOption)
|
|
1096
|
-
|
|
1097
|
-
// Set the value immediately
|
|
1098
|
-
selectElement.value = '1'
|
|
1099
|
-
|
|
1100
|
-
// Wait for mutation observer and any async operations
|
|
1101
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
1102
|
-
|
|
1103
|
-
// Verify the option still exists
|
|
1104
|
-
expect(selectElement.options.length).toBe(1)
|
|
1105
|
-
expect(selectElement.value).toBe('1')
|
|
1106
|
-
expect(selectElement.options[0].value).toBe('1')
|
|
1107
|
-
expect(selectElement.options[0].textContent).toBe('one')
|
|
1108
|
-
|
|
1109
|
-
// Verify SlimSelect also has the option
|
|
1110
|
-
const data = slim.getData()
|
|
1111
|
-
expect(data.length).toBe(1)
|
|
1112
|
-
expect((data[0] as any).value).toBe('1')
|
|
1113
|
-
expect((data[0] as any).text).toBe('one')
|
|
1114
|
-
|
|
1115
|
-
slim.destroy()
|
|
1116
|
-
})
|
|
1117
|
-
|
|
1118
|
-
test('rapid option and value changes should preserve final state', async () => {
|
|
1119
|
-
// Test multiple rapid mutations to ensure queue mechanism works correctly
|
|
1120
|
-
document.body.innerHTML = '<select id="test-select"></select>'
|
|
1121
|
-
|
|
1122
|
-
const selectElement = document.getElementById('test-select') as HTMLSelectElement
|
|
1123
|
-
const slim = new SlimSelect({
|
|
1124
|
-
select: selectElement
|
|
1125
|
-
})
|
|
1126
|
-
|
|
1127
|
-
// Add first option
|
|
1128
|
-
const option1 = document.createElement('option')
|
|
1129
|
-
option1.value = '1'
|
|
1130
|
-
option1.textContent = 'one'
|
|
1131
|
-
selectElement.appendChild(option1)
|
|
1132
|
-
selectElement.value = '1'
|
|
1133
|
-
|
|
1134
|
-
// Immediately add second option
|
|
1135
|
-
const option2 = document.createElement('option')
|
|
1136
|
-
option2.value = '2'
|
|
1137
|
-
option2.textContent = 'two'
|
|
1138
|
-
selectElement.appendChild(option2)
|
|
1139
|
-
selectElement.value = '2'
|
|
1140
|
-
|
|
1141
|
-
// Immediately add third option
|
|
1142
|
-
const option3 = document.createElement('option')
|
|
1143
|
-
option3.value = '3'
|
|
1144
|
-
option3.textContent = 'three'
|
|
1145
|
-
selectElement.appendChild(option3)
|
|
1146
|
-
selectElement.value = '3'
|
|
1147
|
-
|
|
1148
|
-
// Wait for all mutations to process
|
|
1149
|
-
await new Promise((r) => setTimeout(r, 150))
|
|
1150
|
-
|
|
1151
|
-
// Verify all options exist and final value is correct
|
|
1152
|
-
expect(selectElement.options.length).toBe(3)
|
|
1153
|
-
expect(selectElement.value).toBe('3')
|
|
1154
|
-
expect(selectElement.options[0].value).toBe('1')
|
|
1155
|
-
expect(selectElement.options[0].textContent).toBe('one')
|
|
1156
|
-
expect(selectElement.options[1].value).toBe('2')
|
|
1157
|
-
expect(selectElement.options[1].textContent).toBe('two')
|
|
1158
|
-
expect(selectElement.options[2].value).toBe('3')
|
|
1159
|
-
expect(selectElement.options[2].textContent).toBe('three')
|
|
1160
|
-
|
|
1161
|
-
// Verify SlimSelect also has all options
|
|
1162
|
-
const data = slim.getData()
|
|
1163
|
-
expect(data.length).toBe(3)
|
|
1164
|
-
expect((data[0] as any).value).toBe('1')
|
|
1165
|
-
expect((data[0] as any).text).toBe('one')
|
|
1166
|
-
expect((data[1] as any).value).toBe('2')
|
|
1167
|
-
expect((data[1] as any).text).toBe('two')
|
|
1168
|
-
expect((data[2] as any).value).toBe('3')
|
|
1169
|
-
expect((data[2] as any).text).toBe('three')
|
|
1170
|
-
|
|
1171
|
-
// Verify selected value
|
|
1172
|
-
const selected = slim.getSelected()
|
|
1173
|
-
expect(selected).toEqual(['3'])
|
|
1174
|
-
|
|
1175
|
-
slim.destroy()
|
|
1176
|
-
})
|
|
1177
|
-
|
|
1178
|
-
test('should handle removing all options and adding new ones', async () => {
|
|
1179
|
-
// Test scenario: start with options, remove all, then add new ones
|
|
1180
|
-
// This tests that the queue mechanism handles the removal and addition correctly
|
|
1181
|
-
document.body.innerHTML = `
|
|
1182
|
-
<select id="test-select">
|
|
1183
|
-
<option value="old1">Old One</option>
|
|
1184
|
-
<option value="old2">Old Two</option>
|
|
1185
|
-
<option value="old3">Old Three</option>
|
|
1186
|
-
</select>
|
|
1187
|
-
`
|
|
1188
|
-
|
|
1189
|
-
const selectElement = document.getElementById('test-select') as HTMLSelectElement
|
|
1190
|
-
const slim = new SlimSelect({
|
|
1191
|
-
select: selectElement
|
|
1192
|
-
})
|
|
1193
|
-
|
|
1194
|
-
// Verify initial state
|
|
1195
|
-
expect(selectElement.options.length).toBe(3)
|
|
1196
|
-
expect(slim.getData().length).toBe(3)
|
|
1197
|
-
|
|
1198
|
-
// Remove all options (this will trigger mutation observer with empty data, which we skip)
|
|
1199
|
-
selectElement.innerHTML = ''
|
|
1200
|
-
|
|
1201
|
-
// Wait for mutation observer to process the removal
|
|
1202
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
1203
|
-
|
|
1204
|
-
// Verify options are removed from DOM
|
|
1205
|
-
expect(selectElement.options.length).toBe(0)
|
|
1206
|
-
|
|
1207
|
-
// Add completely new set of options one by one
|
|
1208
|
-
const newOption1 = document.createElement('option')
|
|
1209
|
-
newOption1.value = 'new1'
|
|
1210
|
-
newOption1.textContent = 'New One'
|
|
1211
|
-
selectElement.appendChild(newOption1)
|
|
1212
|
-
|
|
1213
|
-
const newOption2 = document.createElement('option')
|
|
1214
|
-
newOption2.value = 'new2'
|
|
1215
|
-
newOption2.textContent = 'New Two'
|
|
1216
|
-
selectElement.appendChild(newOption2)
|
|
1217
|
-
|
|
1218
|
-
const newOption3 = document.createElement('option')
|
|
1219
|
-
newOption3.value = 'new3'
|
|
1220
|
-
newOption3.textContent = 'New Three'
|
|
1221
|
-
selectElement.appendChild(newOption3)
|
|
1222
|
-
|
|
1223
|
-
// Wait for all mutations to process
|
|
1224
|
-
await new Promise((r) => setTimeout(r, 200))
|
|
1225
|
-
|
|
1226
|
-
// The key test: verify new options persist in DOM (not deleted by race condition)
|
|
1227
|
-
expect(selectElement.options.length).toBe(3)
|
|
1228
|
-
expect(selectElement.options[0].value).toBe('new1')
|
|
1229
|
-
expect(selectElement.options[0].textContent).toBe('New One')
|
|
1230
|
-
expect(selectElement.options[1].value).toBe('new2')
|
|
1231
|
-
expect(selectElement.options[1].textContent).toBe('New Two')
|
|
1232
|
-
expect(selectElement.options[2].value).toBe('new3')
|
|
1233
|
-
expect(selectElement.options[2].textContent).toBe('New Three')
|
|
1234
|
-
|
|
1235
|
-
slim.destroy()
|
|
1236
|
-
})
|
|
1237
|
-
|
|
1238
|
-
test('should handle rapidly removing all options', async () => {
|
|
1239
|
-
// Test scenario: start with options, rapidly remove them all
|
|
1240
|
-
document.body.innerHTML = `
|
|
1241
|
-
<select id="test-select">
|
|
1242
|
-
<option value="opt1">Option 1</option>
|
|
1243
|
-
<option value="opt2">Option 2</option>
|
|
1244
|
-
<option value="opt3">Option 3</option>
|
|
1245
|
-
<option value="opt4">Option 4</option>
|
|
1246
|
-
</select>
|
|
1247
|
-
`
|
|
1248
|
-
|
|
1249
|
-
const selectElement = document.getElementById('test-select') as HTMLSelectElement
|
|
1250
|
-
const slim = new SlimSelect({
|
|
1251
|
-
select: selectElement
|
|
1252
|
-
})
|
|
1253
|
-
|
|
1254
|
-
// Set an initial value
|
|
1255
|
-
selectElement.value = 'opt2'
|
|
1256
|
-
|
|
1257
|
-
// Verify initial state
|
|
1258
|
-
expect(selectElement.options.length).toBe(4)
|
|
1259
|
-
expect(selectElement.value).toBe('opt2')
|
|
1260
|
-
expect(slim.getData().length).toBe(4)
|
|
1261
|
-
|
|
1262
|
-
// Rapidly remove all options
|
|
1263
|
-
selectElement.removeChild(selectElement.options[0])
|
|
1264
|
-
selectElement.removeChild(selectElement.options[0])
|
|
1265
|
-
selectElement.removeChild(selectElement.options[0])
|
|
1266
|
-
selectElement.removeChild(selectElement.options[0])
|
|
1267
|
-
|
|
1268
|
-
// Wait for all mutations to process
|
|
1269
|
-
await new Promise((r) => setTimeout(r, 200))
|
|
1270
|
-
|
|
1271
|
-
// Verify all options are removed
|
|
1272
|
-
expect(selectElement.options.length).toBe(0)
|
|
1273
|
-
expect(selectElement.value).toBe('')
|
|
1274
|
-
|
|
1275
|
-
// Verify SlimSelect also has no options
|
|
1276
|
-
const data = slim.getData()
|
|
1277
|
-
expect(data.length).toBe(0)
|
|
1278
|
-
|
|
1279
|
-
// Verify selected value is empty
|
|
1280
|
-
const selected = slim.getSelected()
|
|
1281
|
-
expect(selected).toEqual([])
|
|
1282
|
-
|
|
1283
|
-
slim.destroy()
|
|
1284
|
-
})
|
|
1285
|
-
|
|
1286
|
-
test('should handle option change after first render', async () => {
|
|
1287
|
-
// Test scenario: start with 2 options
|
|
1288
|
-
document.body.innerHTML = `
|
|
1289
|
-
<select id="test-select">
|
|
1290
|
-
<option value="opt1">Option 1</option>
|
|
1291
|
-
<option value="opt2">Option 2</option>
|
|
1292
|
-
</select>
|
|
1293
|
-
`
|
|
1294
|
-
|
|
1295
|
-
const selectElement = document.getElementById('test-select') as HTMLSelectElement
|
|
1296
|
-
const slim = new SlimSelect({
|
|
1297
|
-
select: selectElement
|
|
1298
|
-
})
|
|
1299
|
-
|
|
1300
|
-
// Set an initial value
|
|
1301
|
-
selectElement.value = 'opt2'
|
|
1302
|
-
|
|
1303
|
-
// Verify initial state
|
|
1304
|
-
expect(selectElement.options.length).toBe(2)
|
|
1305
|
-
expect(selectElement.value).toBe('opt2')
|
|
1306
|
-
expect(slim.getData().length).toBe(2)
|
|
1307
|
-
|
|
1308
|
-
// Wait for all mutations to process
|
|
1309
|
-
await new Promise((r) => setTimeout(r, 200))
|
|
1310
|
-
|
|
1311
|
-
// Rebuild options
|
|
1312
|
-
document.getElementById('test-select')!.innerHTML = `
|
|
1313
|
-
<option value="opt1">Option 1 updated</option>
|
|
1314
|
-
<option value="opt2" selected>Option 2 updated</option>
|
|
1315
|
-
`
|
|
1316
|
-
|
|
1317
|
-
// Wait for all mutations to process
|
|
1318
|
-
await new Promise((r) => setTimeout(r, 200))
|
|
1319
|
-
|
|
1320
|
-
// Verify options are still there
|
|
1321
|
-
expect(selectElement.options.length).toBe(2)
|
|
1322
|
-
expect(selectElement.options[0].textContent).toBe('Option 1 updated')
|
|
1323
|
-
expect(selectElement.options[1].textContent).toBe('Option 2 updated')
|
|
1324
|
-
expect(selectElement.value).toBe('opt2')
|
|
1325
|
-
|
|
1326
|
-
// Verify SlimSelect also has the options
|
|
1327
|
-
const data = slim.getData()
|
|
1328
|
-
expect(data.length).toBe(2)
|
|
1329
|
-
|
|
1330
|
-
// Verify selected value is empty
|
|
1331
|
-
const selected = slim.getSelected()
|
|
1332
|
-
expect(selected).toEqual(['opt2'])
|
|
1333
|
-
|
|
1334
|
-
slim.destroy()
|
|
1335
|
-
})
|
|
1336
|
-
})
|
|
1337
|
-
|
|
1338
|
-
describe('cssClasses with space-separated strings', () => {
|
|
1339
|
-
test('space-separated cssClasses are applied as individual classes', () => {
|
|
1340
|
-
document.body.innerHTML = '<select id="test"><option>Test</option></select>'
|
|
1341
|
-
|
|
1342
|
-
const slim = new SlimSelect({
|
|
1343
|
-
select: '#test',
|
|
1344
|
-
cssClasses: {
|
|
1345
|
-
main: 'class1 class2'
|
|
1346
|
-
}
|
|
1347
|
-
})
|
|
1348
|
-
|
|
1349
|
-
expect(slim.render.main.main.classList.contains('ss-main')).toBe(true)
|
|
1350
|
-
expect(slim.render.main.main.classList.contains('class1')).toBe(true)
|
|
1351
|
-
expect(slim.render.main.main.classList.contains('class2')).toBe(true)
|
|
1352
|
-
|
|
1353
|
-
slim.destroy()
|
|
1354
|
-
})
|
|
1355
|
-
})
|
|
1356
|
-
})
|