bard-tag_field 0.5.1 → 0.5.3
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.
- checksums.yaml +4 -4
- data/CLAUDE.md +9 -8
- data/Rakefile +4 -1
- data/app/assets/javascripts/input-tag.js +16 -30
- data/input-tag/.gitignore +6 -2
- data/input-tag/CLAUDE.md +87 -0
- data/input-tag/LICENSE +21 -0
- data/input-tag/README.md +135 -0
- data/input-tag/TESTING.md +99 -0
- data/input-tag/bun.lock +821 -0
- data/input-tag/index.html +331 -0
- data/input-tag/package.json +52 -8
- data/input-tag/rollup.config.js +4 -4
- data/input-tag/src/input-tag.js +849 -0
- data/input-tag/src/taggle.js +546 -0
- data/input-tag/test/api-methods.test.js +684 -0
- data/input-tag/test/autocomplete.test.js +615 -0
- data/input-tag/test/basic-functionality.test.js +567 -0
- data/input-tag/test/dom-mutation.test.js +466 -0
- data/input-tag/test/edge-cases.test.js +524 -0
- data/input-tag/test/events.test.js +425 -0
- data/input-tag/test/form-integration.test.js +447 -0
- data/input-tag/test/input-tag.test.js +90 -0
- data/input-tag/test/lib/fail-only.mjs +24 -0
- data/input-tag/test/lib/test-utils.js +187 -0
- data/input-tag/test/nested-datalist.test.js +328 -0
- data/input-tag/test/value-label-separation.test.js +357 -0
- data/input-tag/web-test-runner.config.mjs +20 -0
- data/lib/bard/tag_field/version.rb +1 -1
- metadata +23 -4
- data/input-tag/bun.lockb +0 -0
- data/input-tag/index.js +0 -1
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import { expect } from '@esm-bundle/chai'
|
|
2
|
+
import { spy } from 'sinon'
|
|
3
|
+
import '../src/input-tag.js'
|
|
4
|
+
import {
|
|
5
|
+
setupGlobalTestHooks,
|
|
6
|
+
setupInputTag,
|
|
7
|
+
waitForUpdate,
|
|
8
|
+
getTagElements,
|
|
9
|
+
getTagValues
|
|
10
|
+
} from './lib/test-utils.js'
|
|
11
|
+
|
|
12
|
+
describe('API Methods', () => {
|
|
13
|
+
setupGlobalTestHooks()
|
|
14
|
+
|
|
15
|
+
describe('Focus Method', () => {
|
|
16
|
+
it('should focus the internal input when focus() is called', async () => {
|
|
17
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
18
|
+
|
|
19
|
+
const focusSpy = spy(inputTag._taggleInputTarget, 'focus')
|
|
20
|
+
|
|
21
|
+
inputTag.focus()
|
|
22
|
+
await waitForUpdate()
|
|
23
|
+
|
|
24
|
+
expect(focusSpy.called).to.be.true
|
|
25
|
+
focusSpy.restore()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should work when called multiple times', async () => {
|
|
29
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
30
|
+
|
|
31
|
+
const focusSpy = spy(inputTag._taggleInputTarget, 'focus')
|
|
32
|
+
|
|
33
|
+
inputTag.focus()
|
|
34
|
+
inputTag.focus()
|
|
35
|
+
inputTag.focus()
|
|
36
|
+
await waitForUpdate()
|
|
37
|
+
|
|
38
|
+
expect(focusSpy.callCount).to.be.at.least(3)
|
|
39
|
+
focusSpy.restore()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should work even when element is not visible', async () => {
|
|
43
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple style="display: none;"></input-tag>')
|
|
44
|
+
|
|
45
|
+
// Should not throw error
|
|
46
|
+
expect(() => {
|
|
47
|
+
inputTag.focus()
|
|
48
|
+
}).to.not.throw()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should handle focus when input-tag is disabled', async () => {
|
|
52
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
53
|
+
|
|
54
|
+
// Disable the component
|
|
55
|
+
inputTag.disable()
|
|
56
|
+
|
|
57
|
+
// Focus should still work at the API level
|
|
58
|
+
expect(() => {
|
|
59
|
+
inputTag.focus()
|
|
60
|
+
}).to.not.throw()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('Reset Method', () => {
|
|
65
|
+
it('should clear all tags when reset() is called', async () => {
|
|
66
|
+
const inputTag = await setupInputTag(`
|
|
67
|
+
<input-tag name="tags" multiple>
|
|
68
|
+
<tag-option value="tag1">Tag 1</tag-option>
|
|
69
|
+
<tag-option value="tag2">Tag 2</tag-option>
|
|
70
|
+
<tag-option value="tag3">Tag 3</tag-option>
|
|
71
|
+
</input-tag>
|
|
72
|
+
`)
|
|
73
|
+
|
|
74
|
+
expect(getTagElements(inputTag)).to.have.length(3)
|
|
75
|
+
|
|
76
|
+
inputTag.reset()
|
|
77
|
+
await waitForUpdate()
|
|
78
|
+
|
|
79
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
80
|
+
expect(getTagValues(inputTag)).to.deep.equal([])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should clear input field when reset() is called', async () => {
|
|
84
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
85
|
+
|
|
86
|
+
inputTag._taggleInputTarget.value = 'pending input'
|
|
87
|
+
|
|
88
|
+
inputTag.reset()
|
|
89
|
+
await waitForUpdate()
|
|
90
|
+
|
|
91
|
+
expect(inputTag._taggleInputTarget.value).to.equal('')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should work on empty input-tag', async () => {
|
|
95
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
96
|
+
|
|
97
|
+
expect(() => {
|
|
98
|
+
inputTag.reset()
|
|
99
|
+
}).to.not.throw()
|
|
100
|
+
|
|
101
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should work multiple times', async () => {
|
|
105
|
+
const inputTag = await setupInputTag(`
|
|
106
|
+
<input-tag name="tags" multiple>
|
|
107
|
+
<tag-option value="test">Test</tag-option>
|
|
108
|
+
</input-tag>
|
|
109
|
+
`)
|
|
110
|
+
|
|
111
|
+
inputTag.reset()
|
|
112
|
+
inputTag.reset()
|
|
113
|
+
inputTag.reset()
|
|
114
|
+
await waitForUpdate()
|
|
115
|
+
|
|
116
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should allow adding tags after reset', async () => {
|
|
120
|
+
const inputTag = await setupInputTag(`
|
|
121
|
+
<input-tag name="tags" multiple>
|
|
122
|
+
<tag-option value="original">Original</tag-option>
|
|
123
|
+
</input-tag>
|
|
124
|
+
`)
|
|
125
|
+
|
|
126
|
+
inputTag.reset()
|
|
127
|
+
await waitForUpdate()
|
|
128
|
+
|
|
129
|
+
inputTag.add('after-reset')
|
|
130
|
+
await waitForUpdate()
|
|
131
|
+
|
|
132
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
133
|
+
expect(getTagValues(inputTag)).to.deep.equal(['after-reset'])
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('Validation Methods', () => {
|
|
138
|
+
describe('checkValidity()', () => {
|
|
139
|
+
it('should return true for valid non-required field', async () => {
|
|
140
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
141
|
+
|
|
142
|
+
expect(inputTag.checkValidity()).to.be.true
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should return false for empty required field', async () => {
|
|
146
|
+
const inputTag = await setupInputTag('<input-tag name="tags" required multiple></input-tag>')
|
|
147
|
+
|
|
148
|
+
expect(inputTag.checkValidity()).to.be.false
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should return true for required field with tags', async () => {
|
|
152
|
+
const inputTag = await setupInputTag(`
|
|
153
|
+
<input-tag name="tags" required multiple>
|
|
154
|
+
<tag-option value="present">Present</tag-option>
|
|
155
|
+
</input-tag>
|
|
156
|
+
`)
|
|
157
|
+
|
|
158
|
+
expect(inputTag.checkValidity()).to.be.true
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should update validity when tags are added to required field', async () => {
|
|
162
|
+
const inputTag = await setupInputTag('<input-tag name="tags" required multiple></input-tag>')
|
|
163
|
+
|
|
164
|
+
expect(inputTag.checkValidity()).to.be.false
|
|
165
|
+
|
|
166
|
+
inputTag.add('new-tag')
|
|
167
|
+
await waitForUpdate()
|
|
168
|
+
|
|
169
|
+
expect(inputTag.checkValidity()).to.be.true
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should update validity when tags are removed from required field', async () => {
|
|
173
|
+
const inputTag = await setupInputTag(`
|
|
174
|
+
<input-tag name="tags" required multiple>
|
|
175
|
+
<tag-option value="only-tag">Only Tag</tag-option>
|
|
176
|
+
</input-tag>
|
|
177
|
+
`)
|
|
178
|
+
|
|
179
|
+
expect(inputTag.checkValidity()).to.be.true
|
|
180
|
+
|
|
181
|
+
inputTag.removeAll()
|
|
182
|
+
await waitForUpdate()
|
|
183
|
+
|
|
184
|
+
expect(inputTag.checkValidity()).to.be.false
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should delegate to internal input validation', async () => {
|
|
188
|
+
const inputTag = await setupInputTag('<input-tag name="tags" required multiple></input-tag>')
|
|
189
|
+
|
|
190
|
+
// Set custom validity on internal input
|
|
191
|
+
inputTag._taggleInputTarget.setCustomValidity('Custom error message')
|
|
192
|
+
|
|
193
|
+
expect(inputTag.checkValidity()).to.be.false
|
|
194
|
+
|
|
195
|
+
// Clear custom validity
|
|
196
|
+
inputTag._taggleInputTarget.setCustomValidity('')
|
|
197
|
+
inputTag.add('valid-tag')
|
|
198
|
+
await waitForUpdate()
|
|
199
|
+
|
|
200
|
+
expect(inputTag.checkValidity()).to.be.true
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('reportValidity()', () => {
|
|
205
|
+
it('should return true for valid field', async () => {
|
|
206
|
+
const inputTag = await setupInputTag(`
|
|
207
|
+
<input-tag name="tags" required multiple>
|
|
208
|
+
<tag-option value="valid">Valid</tag-option>
|
|
209
|
+
</input-tag>
|
|
210
|
+
`)
|
|
211
|
+
|
|
212
|
+
expect(inputTag.reportValidity()).to.be.true
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should return false for invalid field', async () => {
|
|
216
|
+
const inputTag = await setupInputTag('<input-tag name="tags" required multiple></input-tag>')
|
|
217
|
+
|
|
218
|
+
expect(inputTag.reportValidity()).to.be.false
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should show validation message for invalid field', async () => {
|
|
222
|
+
const inputTag = await setupInputTag('<input-tag name="tags" required multiple></input-tag>')
|
|
223
|
+
|
|
224
|
+
// This would normally show browser validation UI
|
|
225
|
+
const result = inputTag.reportValidity()
|
|
226
|
+
|
|
227
|
+
expect(result).to.be.false
|
|
228
|
+
expect(inputTag._taggleInputTarget.validationMessage).to.not.be.empty
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should delegate to internal input reportValidity', async () => {
|
|
232
|
+
const inputTag = await setupInputTag('<input-tag name="tags" required multiple></input-tag>')
|
|
233
|
+
|
|
234
|
+
inputTag._taggleInputTarget.setCustomValidity('Custom validation message')
|
|
235
|
+
|
|
236
|
+
const result = inputTag.reportValidity()
|
|
237
|
+
|
|
238
|
+
expect(result).to.be.false
|
|
239
|
+
expect(inputTag._taggleInputTarget.validationMessage).to.equal('Custom validation message')
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('Value Getter and Setter', () => {
|
|
245
|
+
describe('Value Getter', () => {
|
|
246
|
+
it('should return array of tag values for multiple mode', async () => {
|
|
247
|
+
const inputTag = await setupInputTag(`
|
|
248
|
+
<input-tag name="tags" multiple>
|
|
249
|
+
<tag-option value="tag1">Tag 1</tag-option>
|
|
250
|
+
<tag-option value="tag2">Tag 2</tag-option>
|
|
251
|
+
</input-tag>
|
|
252
|
+
`)
|
|
253
|
+
|
|
254
|
+
expect(inputTag.value).to.deep.equal(['tag1', 'tag2'])
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('should return empty array for no tags in multiple mode', async () => {
|
|
258
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
259
|
+
|
|
260
|
+
expect(inputTag.value).to.deep.equal([])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should return string value for single mode with tag', async () => {
|
|
264
|
+
const inputTag = await setupInputTag(`
|
|
265
|
+
<input-tag name="tag">
|
|
266
|
+
<tag-option value="single-tag">Single Tag</tag-option>
|
|
267
|
+
</input-tag>
|
|
268
|
+
`)
|
|
269
|
+
|
|
270
|
+
expect(inputTag.value).to.equal('single-tag')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('should return empty string for no tags in single mode', async () => {
|
|
274
|
+
const inputTag = await setupInputTag('<input-tag name="tag"></input-tag>')
|
|
275
|
+
|
|
276
|
+
expect(inputTag.value).to.equal('')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('should return current values after modifications in multiple mode', async () => {
|
|
280
|
+
const inputTag = await setupInputTag(`
|
|
281
|
+
<input-tag name="tags" multiple>
|
|
282
|
+
<tag-option value="initial">Initial</tag-option>
|
|
283
|
+
</input-tag>
|
|
284
|
+
`)
|
|
285
|
+
|
|
286
|
+
expect(inputTag.value).to.deep.equal(['initial'])
|
|
287
|
+
|
|
288
|
+
inputTag.add('added')
|
|
289
|
+
await waitForUpdate()
|
|
290
|
+
|
|
291
|
+
expect(inputTag.value).to.deep.equal(['initial', 'added'])
|
|
292
|
+
|
|
293
|
+
inputTag.remove('initial')
|
|
294
|
+
await waitForUpdate()
|
|
295
|
+
|
|
296
|
+
expect(inputTag.value).to.deep.equal(['added'])
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('should return current value after modifications in single mode', async () => {
|
|
300
|
+
const inputTag = await setupInputTag(`
|
|
301
|
+
<input-tag name="tag">
|
|
302
|
+
<tag-option value="initial">Initial</tag-option>
|
|
303
|
+
</input-tag>
|
|
304
|
+
`)
|
|
305
|
+
|
|
306
|
+
expect(inputTag.value).to.equal('initial')
|
|
307
|
+
|
|
308
|
+
inputTag.remove('initial')
|
|
309
|
+
await waitForUpdate()
|
|
310
|
+
|
|
311
|
+
expect(inputTag.value).to.equal('')
|
|
312
|
+
|
|
313
|
+
inputTag.add('replacement')
|
|
314
|
+
await waitForUpdate()
|
|
315
|
+
|
|
316
|
+
expect(inputTag.value).to.equal('replacement')
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('Value Setter', () => {
|
|
321
|
+
it('should set tag values from array in multiple mode', async () => {
|
|
322
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
323
|
+
|
|
324
|
+
inputTag.value = ['new1', 'new2', 'new3']
|
|
325
|
+
await waitForUpdate()
|
|
326
|
+
|
|
327
|
+
expect(getTagElements(inputTag)).to.have.length(3)
|
|
328
|
+
expect(getTagValues(inputTag)).to.deep.equal(['new1', 'new2', 'new3'])
|
|
329
|
+
expect(inputTag.value).to.deep.equal(['new1', 'new2', 'new3'])
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('should set tag value from string in single mode', async () => {
|
|
333
|
+
const inputTag = await setupInputTag('<input-tag name="tag"></input-tag>')
|
|
334
|
+
|
|
335
|
+
inputTag.value = 'single-tag'
|
|
336
|
+
await waitForUpdate()
|
|
337
|
+
|
|
338
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
339
|
+
expect(getTagValues(inputTag)).to.deep.equal(['single-tag'])
|
|
340
|
+
expect(inputTag.value).to.equal('single-tag')
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('should replace existing tags when setting array value in multiple mode', async () => {
|
|
344
|
+
const inputTag = await setupInputTag(`
|
|
345
|
+
<input-tag name="tags" multiple>
|
|
346
|
+
<tag-option value="old1">Old 1</tag-option>
|
|
347
|
+
<tag-option value="old2">Old 2</tag-option>
|
|
348
|
+
</input-tag>
|
|
349
|
+
`)
|
|
350
|
+
|
|
351
|
+
inputTag.value = ['replacement1', 'replacement2']
|
|
352
|
+
await waitForUpdate()
|
|
353
|
+
|
|
354
|
+
expect(getTagElements(inputTag)).to.have.length(2)
|
|
355
|
+
expect(getTagValues(inputTag)).to.deep.equal(['replacement1', 'replacement2'])
|
|
356
|
+
expect(inputTag.value).to.deep.equal(['replacement1', 'replacement2'])
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('should replace existing tag when setting string value in single mode', async () => {
|
|
360
|
+
const inputTag = await setupInputTag(`
|
|
361
|
+
<input-tag name="tag">
|
|
362
|
+
<tag-option value="old">Old</tag-option>
|
|
363
|
+
</input-tag>
|
|
364
|
+
`)
|
|
365
|
+
|
|
366
|
+
inputTag.value = 'replacement'
|
|
367
|
+
await waitForUpdate()
|
|
368
|
+
|
|
369
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
370
|
+
expect(getTagValues(inputTag)).to.deep.equal(['replacement'])
|
|
371
|
+
expect(inputTag.value).to.equal('replacement')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('should clear tags when setting empty array in multiple mode', async () => {
|
|
375
|
+
const inputTag = await setupInputTag(`
|
|
376
|
+
<input-tag name="tags" multiple>
|
|
377
|
+
<tag-option value="will-be-cleared">Will Be Cleared</tag-option>
|
|
378
|
+
</input-tag>
|
|
379
|
+
`)
|
|
380
|
+
|
|
381
|
+
inputTag.value = []
|
|
382
|
+
await waitForUpdate()
|
|
383
|
+
|
|
384
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
385
|
+
expect(getTagValues(inputTag)).to.deep.equal([])
|
|
386
|
+
expect(inputTag.value).to.deep.equal([])
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('should clear tag when setting empty string in single mode', async () => {
|
|
390
|
+
const inputTag = await setupInputTag(`
|
|
391
|
+
<input-tag name="tag">
|
|
392
|
+
<tag-option value="will-be-cleared">Will Be Cleared</tag-option>
|
|
393
|
+
</input-tag>
|
|
394
|
+
`)
|
|
395
|
+
|
|
396
|
+
inputTag.value = ''
|
|
397
|
+
await waitForUpdate()
|
|
398
|
+
|
|
399
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
400
|
+
expect(getTagValues(inputTag)).to.deep.equal([])
|
|
401
|
+
expect(inputTag.value).to.equal('')
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('should handle string value in single mode', async () => {
|
|
405
|
+
const inputTag = await setupInputTag('<input-tag name="tag"></input-tag>')
|
|
406
|
+
|
|
407
|
+
inputTag.value = 'single-value'
|
|
408
|
+
await waitForUpdate()
|
|
409
|
+
|
|
410
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
411
|
+
expect(getTagValues(inputTag)).to.deep.equal(['single-value'])
|
|
412
|
+
expect(inputTag.value).to.equal('single-value')
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('should handle array value in single mode (for backward compatibility)', async () => {
|
|
416
|
+
const inputTag = await setupInputTag('<input-tag name="tag"></input-tag>')
|
|
417
|
+
|
|
418
|
+
inputTag.value = ['single-value']
|
|
419
|
+
await waitForUpdate()
|
|
420
|
+
|
|
421
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
422
|
+
expect(getTagValues(inputTag)).to.deep.equal(['single-value'])
|
|
423
|
+
expect(inputTag.value).to.equal('single-value')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('should trigger change event when array value is set programmatically in multiple mode', async () => {
|
|
427
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
428
|
+
|
|
429
|
+
let changeEventFired = false
|
|
430
|
+
inputTag.addEventListener('change', () => {
|
|
431
|
+
changeEventFired = true
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
inputTag.value = ['programmatic']
|
|
435
|
+
await waitForUpdate()
|
|
436
|
+
|
|
437
|
+
expect(changeEventFired).to.be.true
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('should trigger change event when string value is set programmatically in single mode', async () => {
|
|
441
|
+
const inputTag = await setupInputTag('<input-tag name="tag"></input-tag>')
|
|
442
|
+
|
|
443
|
+
let changeEventFired = false
|
|
444
|
+
inputTag.addEventListener('change', () => {
|
|
445
|
+
changeEventFired = true
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
inputTag.value = 'programmatic'
|
|
449
|
+
await waitForUpdate()
|
|
450
|
+
|
|
451
|
+
expect(changeEventFired).to.be.true
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it('should not trigger change event when array value is set to same value in multiple mode', async () => {
|
|
455
|
+
const inputTag = await setupInputTag(`
|
|
456
|
+
<input-tag name="tags" multiple>
|
|
457
|
+
<tag-option value="same">Same</tag-option>
|
|
458
|
+
</input-tag>
|
|
459
|
+
`)
|
|
460
|
+
|
|
461
|
+
let changeEventFired = false
|
|
462
|
+
inputTag.addEventListener('change', () => {
|
|
463
|
+
changeEventFired = true
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
inputTag.value = ['same']
|
|
467
|
+
await waitForUpdate()
|
|
468
|
+
|
|
469
|
+
expect(changeEventFired).to.be.false
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('should not trigger change event when string value is set to same value in single mode', async () => {
|
|
473
|
+
const inputTag = await setupInputTag(`
|
|
474
|
+
<input-tag name="tag">
|
|
475
|
+
<tag-option value="same">Same</tag-option>
|
|
476
|
+
</input-tag>
|
|
477
|
+
`)
|
|
478
|
+
|
|
479
|
+
let changeEventFired = false
|
|
480
|
+
inputTag.addEventListener('change', () => {
|
|
481
|
+
changeEventFired = true
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
inputTag.value = 'same'
|
|
485
|
+
await waitForUpdate()
|
|
486
|
+
|
|
487
|
+
expect(changeEventFired).to.be.false
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
describe('Property Getters', () => {
|
|
493
|
+
describe('Form Property', () => {
|
|
494
|
+
it('should return associated form element', async () => {
|
|
495
|
+
document.body.innerHTML = `
|
|
496
|
+
<form id="test-form">
|
|
497
|
+
<input-tag name="tags" multiple></input-tag>
|
|
498
|
+
</form>
|
|
499
|
+
`
|
|
500
|
+
const form = document.querySelector('#test-form')
|
|
501
|
+
const inputTag = document.querySelector('input-tag')
|
|
502
|
+
|
|
503
|
+
await waitForUpdate(100)
|
|
504
|
+
|
|
505
|
+
expect(inputTag.form).to.equal(form)
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('should return null when not in a form', async () => {
|
|
509
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
510
|
+
|
|
511
|
+
expect(inputTag.form).to.be.null
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
describe('Name Property', () => {
|
|
516
|
+
it('should return name attribute value', async () => {
|
|
517
|
+
const inputTag = await setupInputTag('<input-tag name="test-name" multiple></input-tag>')
|
|
518
|
+
|
|
519
|
+
expect(inputTag.name).to.equal('test-name')
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
it('should return null when name attribute is not set', async () => {
|
|
523
|
+
const inputTag = await setupInputTag('<input-tag multiple></input-tag>')
|
|
524
|
+
|
|
525
|
+
expect(inputTag.name).to.be.null
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it('should update when name attribute changes', async () => {
|
|
529
|
+
const inputTag = await setupInputTag('<input-tag name="original-name" multiple></input-tag>')
|
|
530
|
+
|
|
531
|
+
expect(inputTag.name).to.equal('original-name')
|
|
532
|
+
|
|
533
|
+
inputTag.setAttribute('name', 'updated-name')
|
|
534
|
+
expect(inputTag.name).to.equal('updated-name')
|
|
535
|
+
})
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
describe('Options Property', () => {
|
|
539
|
+
it('should return options from associated datalist', async () => {
|
|
540
|
+
document.body.innerHTML = `
|
|
541
|
+
<input-tag name="tags" list="test-list" multiple></input-tag>
|
|
542
|
+
<datalist id="test-list">
|
|
543
|
+
<option value="option1">Option 1</option>
|
|
544
|
+
<option value="option2">Option 2</option>
|
|
545
|
+
</datalist>
|
|
546
|
+
`
|
|
547
|
+
const inputTag = document.querySelector('input-tag')
|
|
548
|
+
|
|
549
|
+
await waitForUpdate(100)
|
|
550
|
+
|
|
551
|
+
expect(inputTag.options).to.deep.equal(['option1', 'option2'])
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('should return empty array when no datalist is associated', async () => {
|
|
555
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
556
|
+
|
|
557
|
+
expect(inputTag.options).to.deep.equal([])
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it('should update when datalist changes', async () => {
|
|
561
|
+
document.body.innerHTML = `
|
|
562
|
+
<input-tag name="tags" list="dynamic-list" multiple></input-tag>
|
|
563
|
+
<datalist id="dynamic-list">
|
|
564
|
+
<option value="initial">Initial</option>
|
|
565
|
+
</datalist>
|
|
566
|
+
`
|
|
567
|
+
const inputTag = document.querySelector('input-tag')
|
|
568
|
+
const datalist = document.querySelector('#dynamic-list')
|
|
569
|
+
|
|
570
|
+
await waitForUpdate(100)
|
|
571
|
+
|
|
572
|
+
expect(inputTag.options).to.deep.equal(['initial'])
|
|
573
|
+
|
|
574
|
+
const newOption = document.createElement('option')
|
|
575
|
+
newOption.value = 'added'
|
|
576
|
+
datalist.appendChild(newOption)
|
|
577
|
+
|
|
578
|
+
expect(inputTag.options).to.deep.equal(['initial', 'added'])
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
describe('Public API Methods', () => {
|
|
584
|
+
it('should provide add/remove/removeAll methods', async () => {
|
|
585
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
586
|
+
|
|
587
|
+
expect(typeof inputTag.add).to.equal('function')
|
|
588
|
+
expect(typeof inputTag.remove).to.equal('function')
|
|
589
|
+
expect(typeof inputTag.removeAll).to.equal('function')
|
|
590
|
+
expect(typeof inputTag.has).to.equal('function')
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('should allow manipulation via public API methods', async () => {
|
|
594
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
595
|
+
|
|
596
|
+
// Test add
|
|
597
|
+
inputTag.add('public-api-tag')
|
|
598
|
+
await waitForUpdate()
|
|
599
|
+
|
|
600
|
+
expect(getTagValues(inputTag)).to.deep.equal(['public-api-tag'])
|
|
601
|
+
|
|
602
|
+
// Test tags getter
|
|
603
|
+
expect(inputTag.tags).to.deep.equal(['public-api-tag'])
|
|
604
|
+
|
|
605
|
+
// Test has
|
|
606
|
+
expect(inputTag.has('public-api-tag')).to.be.true
|
|
607
|
+
expect(inputTag.has('nonexistent')).to.be.false
|
|
608
|
+
|
|
609
|
+
// Test remove
|
|
610
|
+
inputTag.remove('public-api-tag')
|
|
611
|
+
await waitForUpdate()
|
|
612
|
+
|
|
613
|
+
expect(getTagValues(inputTag)).to.deep.equal([])
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it('should support disable and enable methods', async () => {
|
|
617
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
618
|
+
|
|
619
|
+
expect(typeof inputTag.disable).to.equal('function')
|
|
620
|
+
expect(typeof inputTag.enable).to.equal('function')
|
|
621
|
+
|
|
622
|
+
// These should not throw
|
|
623
|
+
expect(() => {
|
|
624
|
+
inputTag.disable()
|
|
625
|
+
inputTag.enable()
|
|
626
|
+
}).to.not.throw()
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
it('should support addAt method for positional insertion', async () => {
|
|
630
|
+
const inputTag = await setupInputTag(`
|
|
631
|
+
<input-tag name="tags" multiple>
|
|
632
|
+
<tag-option value="first">First</tag-option>
|
|
633
|
+
<tag-option value="third">Third</tag-option>
|
|
634
|
+
</input-tag>
|
|
635
|
+
`)
|
|
636
|
+
|
|
637
|
+
expect(typeof inputTag.addAt).to.equal('function')
|
|
638
|
+
|
|
639
|
+
inputTag.addAt('second', 1)
|
|
640
|
+
await waitForUpdate()
|
|
641
|
+
|
|
642
|
+
expect(inputTag.tags).to.include.members(['first', 'third', 'second'])
|
|
643
|
+
})
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
describe('Error Handling in API Methods', () => {
|
|
647
|
+
it('should handle invalid parameters gracefully', async () => {
|
|
648
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
649
|
+
|
|
650
|
+
// These should not throw errors
|
|
651
|
+
expect(() => {
|
|
652
|
+
inputTag.focus(null)
|
|
653
|
+
inputTag.reset(undefined)
|
|
654
|
+
inputTag.checkValidity('invalid-param')
|
|
655
|
+
inputTag.reportValidity({})
|
|
656
|
+
}).to.not.throw()
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
it('should handle API calls before initialization', async () => {
|
|
660
|
+
document.body.innerHTML = '<input-tag name="tags" multiple></input-tag>'
|
|
661
|
+
const inputTag = document.querySelector('input-tag')
|
|
662
|
+
|
|
663
|
+
// These should not throw before internals are initialized
|
|
664
|
+
expect(() => {
|
|
665
|
+
inputTag.focus()
|
|
666
|
+
inputTag.checkValidity()
|
|
667
|
+
inputTag.reportValidity()
|
|
668
|
+
}).to.not.throw()
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
it('should handle API calls after disconnect', async () => {
|
|
672
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
673
|
+
|
|
674
|
+
inputTag.disconnectedCallback()
|
|
675
|
+
|
|
676
|
+
// Basic API methods should still work or fail gracefully
|
|
677
|
+
expect(() => {
|
|
678
|
+
inputTag.focus()
|
|
679
|
+
inputTag.checkValidity()
|
|
680
|
+
inputTag.reportValidity()
|
|
681
|
+
}).to.not.throw()
|
|
682
|
+
})
|
|
683
|
+
})
|
|
684
|
+
})
|