bard-tag_field 0.5.0 → 0.5.2
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 -2
- data/app/assets/javascripts/input-tag.js +1 -0
- data/bard-tag_field.gemspec +5 -0
- data/cucumber.yml +1 -0
- 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/cucumber.rb +13 -2
- data/lib/bard/tag_field/field.rb +3 -2
- data/lib/bard/tag_field/version.rb +1 -1
- metadata +97 -7
- data/app/assets/javascripts/input-tag.js +0 -1859
- data/input-tag/bun.lockb +0 -0
- data/input-tag/index.js +0 -1
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { expect } from '@esm-bundle/chai'
|
|
2
|
+
import '../src/input-tag.js'
|
|
3
|
+
import {
|
|
4
|
+
setupGlobalTestHooks,
|
|
5
|
+
setupInputTag,
|
|
6
|
+
waitForElement,
|
|
7
|
+
waitForBasicInitialization,
|
|
8
|
+
waitForUpdate,
|
|
9
|
+
simulateInput,
|
|
10
|
+
simulateKeydown,
|
|
11
|
+
simulateKeyup,
|
|
12
|
+
getTagElements,
|
|
13
|
+
getTagValues,
|
|
14
|
+
KEYCODES
|
|
15
|
+
} from './lib/test-utils.js'
|
|
16
|
+
|
|
17
|
+
describe('Edge Cases', () => {
|
|
18
|
+
setupGlobalTestHooks()
|
|
19
|
+
|
|
20
|
+
describe('Empty String Handling', () => {
|
|
21
|
+
it('should not add tags for empty strings', async () => {
|
|
22
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
23
|
+
|
|
24
|
+
inputTag.add('')
|
|
25
|
+
await waitForUpdate()
|
|
26
|
+
|
|
27
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should not add tags for empty input via keyboard', async () => {
|
|
31
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
32
|
+
const input = inputTag._taggleInputTarget
|
|
33
|
+
|
|
34
|
+
await simulateInput(input, '')
|
|
35
|
+
simulateKeydown(input, KEYCODES.ENTER)
|
|
36
|
+
await waitForUpdate()
|
|
37
|
+
|
|
38
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should not add tags for empty input via button click', async () => {
|
|
42
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
43
|
+
const input = inputTag._taggleInputTarget
|
|
44
|
+
const button = inputTag.buttonTarget
|
|
45
|
+
|
|
46
|
+
await simulateInput(input, '')
|
|
47
|
+
button.click()
|
|
48
|
+
await waitForUpdate()
|
|
49
|
+
|
|
50
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('Whitespace-Only Input', () => {
|
|
55
|
+
it('should not add tags for whitespace-only strings', async () => {
|
|
56
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
57
|
+
|
|
58
|
+
inputTag.add(' ')
|
|
59
|
+
inputTag.add('\n\t ')
|
|
60
|
+
inputTag.add(' \r\n ')
|
|
61
|
+
await waitForUpdate()
|
|
62
|
+
|
|
63
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should trim whitespace from valid tags', async () => {
|
|
67
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
68
|
+
|
|
69
|
+
inputTag.add(' valid-tag ')
|
|
70
|
+
inputTag.add('\n another-tag\t')
|
|
71
|
+
await waitForUpdate()
|
|
72
|
+
|
|
73
|
+
expect(getTagElements(inputTag)).to.have.length(2)
|
|
74
|
+
expect(getTagValues(inputTag)).to.deep.equal(['valid-tag', 'another-tag'])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should handle mixed whitespace and comma separation', async () => {
|
|
78
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
79
|
+
|
|
80
|
+
inputTag.add(' tag1 , tag2 , tag3 ')
|
|
81
|
+
await waitForUpdate()
|
|
82
|
+
|
|
83
|
+
expect(getTagElements(inputTag)).to.have.length(3)
|
|
84
|
+
expect(getTagValues(inputTag)).to.deep.equal(['tag1', 'tag2', 'tag3'])
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('Duplicate Tag Prevention', () => {
|
|
89
|
+
it('should prevent exact duplicate tags', async () => {
|
|
90
|
+
const inputTag = await setupInputTag(`
|
|
91
|
+
<input-tag name="tags" multiple>
|
|
92
|
+
<tag-option value="existing">Existing</tag-option>
|
|
93
|
+
</input-tag>
|
|
94
|
+
`)
|
|
95
|
+
|
|
96
|
+
inputTag.add('existing')
|
|
97
|
+
await waitForUpdate()
|
|
98
|
+
|
|
99
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
100
|
+
expect(getTagValues(inputTag)).to.deep.equal(['existing'])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should allow case-sensitive variations but prevent exact duplicates', async () => {
|
|
104
|
+
const inputTag = await setupInputTag(`
|
|
105
|
+
<input-tag name="tags" multiple>
|
|
106
|
+
<tag-option value="javascript">JavaScript</tag-option>
|
|
107
|
+
</input-tag>
|
|
108
|
+
`)
|
|
109
|
+
|
|
110
|
+
// These are all considered different (case-sensitive)
|
|
111
|
+
inputTag.add('JavaScript')
|
|
112
|
+
inputTag.add('JAVASCRIPT')
|
|
113
|
+
inputTag.add('javaScript')
|
|
114
|
+
await waitForUpdate()
|
|
115
|
+
|
|
116
|
+
expect(getTagElements(inputTag)).to.have.length(4)
|
|
117
|
+
expect(getTagValues(inputTag)).to.deep.equal(['javascript', 'JavaScript', 'JAVASCRIPT', 'javaScript'])
|
|
118
|
+
|
|
119
|
+
// Exact duplicate should be prevented
|
|
120
|
+
inputTag.add('javascript')
|
|
121
|
+
await waitForUpdate()
|
|
122
|
+
|
|
123
|
+
expect(getTagElements(inputTag)).to.have.length(4) // No new tag added
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should prevent duplicates with whitespace variations', async () => {
|
|
127
|
+
const inputTag = await setupInputTag(`
|
|
128
|
+
<input-tag name="tags" multiple>
|
|
129
|
+
<tag-option value="react">React</tag-option>
|
|
130
|
+
</input-tag>
|
|
131
|
+
`)
|
|
132
|
+
|
|
133
|
+
inputTag.add(' react ')
|
|
134
|
+
inputTag.add('\treact\n')
|
|
135
|
+
await waitForUpdate()
|
|
136
|
+
|
|
137
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
138
|
+
expect(getTagValues(inputTag)).to.deep.equal(['react'])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should allow similar but different tags', async () => {
|
|
142
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
143
|
+
|
|
144
|
+
inputTag.add('react')
|
|
145
|
+
inputTag.add('react-native')
|
|
146
|
+
inputTag.add('reactjs')
|
|
147
|
+
await waitForUpdate()
|
|
148
|
+
|
|
149
|
+
expect(getTagElements(inputTag)).to.have.length(3)
|
|
150
|
+
expect(getTagValues(inputTag)).to.deep.equal(['react', 'react-native', 'reactjs'])
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('Very Long Tag Names', () => {
|
|
155
|
+
it('should handle very long tag names', async () => {
|
|
156
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
157
|
+
|
|
158
|
+
const longTag = 'a'.repeat(1000)
|
|
159
|
+
inputTag.add(longTag)
|
|
160
|
+
await waitForUpdate()
|
|
161
|
+
|
|
162
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
163
|
+
expect(getTagValues(inputTag)[0]).to.equal(longTag)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should handle multiple very long tag names', async () => {
|
|
167
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
168
|
+
|
|
169
|
+
const longTag1 = 'x'.repeat(500) + '1'
|
|
170
|
+
const longTag2 = 'y'.repeat(500) + '2'
|
|
171
|
+
|
|
172
|
+
inputTag.add(longTag1)
|
|
173
|
+
inputTag.add(longTag2)
|
|
174
|
+
await waitForUpdate()
|
|
175
|
+
|
|
176
|
+
expect(getTagElements(inputTag)).to.have.length(2)
|
|
177
|
+
expect(getTagValues(inputTag)).to.deep.equal([longTag1, longTag2])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should handle extremely long single line input', async () => {
|
|
181
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
182
|
+
|
|
183
|
+
const veryLongInput = 'tag'.repeat(1000)
|
|
184
|
+
inputTag.add(veryLongInput)
|
|
185
|
+
await waitForUpdate()
|
|
186
|
+
|
|
187
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
188
|
+
expect(getTagValues(inputTag)[0]).to.equal(veryLongInput)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('Special Characters in Tags', () => {
|
|
193
|
+
it('should handle tags with special characters', async () => {
|
|
194
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
195
|
+
|
|
196
|
+
const specialTags = [
|
|
197
|
+
'tag-with-dashes',
|
|
198
|
+
'tag_with_underscores',
|
|
199
|
+
'tag.with.dots',
|
|
200
|
+
'tag@with@symbols',
|
|
201
|
+
'tag#with#hash',
|
|
202
|
+
'tag$with$dollar',
|
|
203
|
+
'tag%with%percent'
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
specialTags.forEach(tag => inputTag.add(tag))
|
|
207
|
+
await waitForUpdate()
|
|
208
|
+
|
|
209
|
+
expect(getTagElements(inputTag)).to.have.length(specialTags.length)
|
|
210
|
+
expect(getTagValues(inputTag)).to.deep.equal(specialTags)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should handle tags with unicode characters', async () => {
|
|
214
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
215
|
+
|
|
216
|
+
const unicodeTags = [
|
|
217
|
+
'café',
|
|
218
|
+
'naïve',
|
|
219
|
+
'résumé',
|
|
220
|
+
'日本語',
|
|
221
|
+
'한국어',
|
|
222
|
+
'العربية',
|
|
223
|
+
'🚀rocket',
|
|
224
|
+
'💻code'
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
unicodeTags.forEach(tag => inputTag.add(tag))
|
|
228
|
+
await waitForUpdate()
|
|
229
|
+
|
|
230
|
+
expect(getTagElements(inputTag)).to.have.length(unicodeTags.length)
|
|
231
|
+
expect(getTagValues(inputTag)).to.deep.equal(unicodeTags)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('should handle tags with quotes and HTML-like content', async () => {
|
|
235
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
236
|
+
|
|
237
|
+
const htmlTags = [
|
|
238
|
+
'"quoted-tag"',
|
|
239
|
+
"'single-quoted'",
|
|
240
|
+
'<not-html>',
|
|
241
|
+
'&special&entities&',
|
|
242
|
+
'tag"with"quotes',
|
|
243
|
+
"tag'with'apostrophes"
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
htmlTags.forEach(tag => inputTag.add(tag))
|
|
247
|
+
await waitForUpdate()
|
|
248
|
+
|
|
249
|
+
expect(getTagElements(inputTag)).to.have.length(htmlTags.length)
|
|
250
|
+
expect(getTagValues(inputTag)).to.deep.equal(htmlTags)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should handle tags with newlines and line breaks', async () => {
|
|
254
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
255
|
+
|
|
256
|
+
// These should be trimmed or handled appropriately
|
|
257
|
+
inputTag.add('tag\nwith\nlines')
|
|
258
|
+
inputTag.add('tag\rwith\rreturns')
|
|
259
|
+
inputTag.add('tag\twith\ttabs')
|
|
260
|
+
await waitForUpdate()
|
|
261
|
+
|
|
262
|
+
expect(getTagElements(inputTag)).to.have.length(3)
|
|
263
|
+
|
|
264
|
+
// The exact behavior may vary, but should not break the component
|
|
265
|
+
const values = getTagValues(inputTag)
|
|
266
|
+
expect(values).to.have.length(3)
|
|
267
|
+
values.forEach(value => {
|
|
268
|
+
expect(value).to.be.a('string')
|
|
269
|
+
expect(value.length).to.be.greaterThan(0)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('Rapid Input Changes', () => {
|
|
275
|
+
it('should handle rapid tag additions', async () => {
|
|
276
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
277
|
+
|
|
278
|
+
// Add many tags rapidly
|
|
279
|
+
for (let i = 0; i < 50; i++) {
|
|
280
|
+
inputTag.add(`rapid-tag-${i}`)
|
|
281
|
+
}
|
|
282
|
+
await waitForUpdate(100)
|
|
283
|
+
|
|
284
|
+
expect(getTagElements(inputTag)).to.have.length(50)
|
|
285
|
+
|
|
286
|
+
const values = getTagValues(inputTag)
|
|
287
|
+
for (let i = 0; i < 50; i++) {
|
|
288
|
+
expect(values).to.include(`rapid-tag-${i}`)
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should handle rapid tag removals', async () => {
|
|
293
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
294
|
+
|
|
295
|
+
// Add many tags first
|
|
296
|
+
const tags = []
|
|
297
|
+
for (let i = 0; i < 20; i++) {
|
|
298
|
+
const tag = `removal-tag-${i}`
|
|
299
|
+
tags.push(tag)
|
|
300
|
+
inputTag.add(tag)
|
|
301
|
+
}
|
|
302
|
+
await waitForUpdate()
|
|
303
|
+
|
|
304
|
+
expect(getTagElements(inputTag)).to.have.length(20)
|
|
305
|
+
|
|
306
|
+
// Remove them rapidly
|
|
307
|
+
tags.forEach(tag => inputTag.remove(tag))
|
|
308
|
+
await waitForUpdate(100)
|
|
309
|
+
|
|
310
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('should handle rapid mixed operations', async () => {
|
|
314
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
315
|
+
|
|
316
|
+
// Mix of rapid additions and removals
|
|
317
|
+
inputTag.add('tag1')
|
|
318
|
+
inputTag.add('tag2')
|
|
319
|
+
inputTag.remove('tag1')
|
|
320
|
+
inputTag.add('tag3')
|
|
321
|
+
inputTag.add('tag4')
|
|
322
|
+
inputTag.remove('tag2')
|
|
323
|
+
inputTag.add('tag5')
|
|
324
|
+
|
|
325
|
+
await waitForUpdate(100)
|
|
326
|
+
|
|
327
|
+
expect(getTagElements(inputTag)).to.have.length(3)
|
|
328
|
+
expect(getTagValues(inputTag)).to.deep.equal(['tag3', 'tag4', 'tag5'])
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('should handle rapid keyboard input', async () => {
|
|
332
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
333
|
+
const input = inputTag._taggleInputTarget
|
|
334
|
+
|
|
335
|
+
// Simulate rapid typing and entering
|
|
336
|
+
await simulateInput(input, 'rapid1')
|
|
337
|
+
simulateKeydown(input, KEYCODES.ENTER)
|
|
338
|
+
await simulateInput(input, 'rapid2')
|
|
339
|
+
simulateKeydown(input, KEYCODES.ENTER)
|
|
340
|
+
await simulateInput(input, 'rapid3')
|
|
341
|
+
simulateKeydown(input, KEYCODES.ENTER)
|
|
342
|
+
|
|
343
|
+
await waitForUpdate(100)
|
|
344
|
+
|
|
345
|
+
expect(getTagElements(inputTag)).to.have.length(3)
|
|
346
|
+
expect(getTagValues(inputTag)).to.deep.equal(['rapid1', 'rapid2', 'rapid3'])
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
describe('Memory Leaks and DOM Cleanup', () => {
|
|
351
|
+
it('should clean up observers on disconnect', async () => {
|
|
352
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
353
|
+
|
|
354
|
+
expect(inputTag.observer).to.not.be.null
|
|
355
|
+
|
|
356
|
+
// Simulate disconnection
|
|
357
|
+
inputTag.disconnectedCallback()
|
|
358
|
+
|
|
359
|
+
// Observer should be cleaned up - no way to directly test this
|
|
360
|
+
// but we can ensure no errors are thrown
|
|
361
|
+
expect(() => {
|
|
362
|
+
document.body.innerHTML = ''
|
|
363
|
+
}).to.not.throw()
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('should handle being removed and re-added to DOM', async () => {
|
|
367
|
+
document.body.innerHTML = `
|
|
368
|
+
<div id="container">
|
|
369
|
+
<input-tag name="tags" multiple>
|
|
370
|
+
<tag-option value="persistent">Persistent</tag-option>
|
|
371
|
+
</input-tag>
|
|
372
|
+
</div>
|
|
373
|
+
`
|
|
374
|
+
|
|
375
|
+
const container = document.querySelector('#container')
|
|
376
|
+
const inputTag = document.querySelector('input-tag')
|
|
377
|
+
|
|
378
|
+
await waitForBasicInitialization(inputTag)
|
|
379
|
+
|
|
380
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
381
|
+
|
|
382
|
+
// Remove from DOM
|
|
383
|
+
const removed = container.removeChild(inputTag)
|
|
384
|
+
await waitForUpdate()
|
|
385
|
+
|
|
386
|
+
// Re-add to DOM
|
|
387
|
+
container.appendChild(removed)
|
|
388
|
+
await waitForUpdate()
|
|
389
|
+
|
|
390
|
+
// Should still work
|
|
391
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
392
|
+
expect(getTagValues(inputTag)).to.deep.equal(['persistent'])
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('should handle multiple instances without interference', async () => {
|
|
396
|
+
document.body.innerHTML = `
|
|
397
|
+
<input-tag name="tags1" multiple></input-tag>
|
|
398
|
+
<input-tag name="tags2" multiple></input-tag>
|
|
399
|
+
<input-tag name="tags3" multiple></input-tag>
|
|
400
|
+
`
|
|
401
|
+
|
|
402
|
+
const inputTag1 = document.querySelector('input-tag[name="tags1"]')
|
|
403
|
+
const inputTag2 = document.querySelector('input-tag[name="tags2"]')
|
|
404
|
+
const inputTag3 = document.querySelector('input-tag[name="tags3"]')
|
|
405
|
+
|
|
406
|
+
await waitForElement(inputTag1, '_taggle')
|
|
407
|
+
await waitForElement(inputTag2, '_taggle')
|
|
408
|
+
await waitForElement(inputTag3, '_taggle')
|
|
409
|
+
|
|
410
|
+
// Add different tags to each
|
|
411
|
+
inputTag1.add('tag1-a')
|
|
412
|
+
inputTag2.add('tag2-a')
|
|
413
|
+
inputTag3.add('tag3-a')
|
|
414
|
+
|
|
415
|
+
await waitForUpdate()
|
|
416
|
+
|
|
417
|
+
expect(getTagValues(inputTag1)).to.deep.equal(['tag1-a'])
|
|
418
|
+
expect(getTagValues(inputTag2)).to.deep.equal(['tag2-a'])
|
|
419
|
+
expect(getTagValues(inputTag3)).to.deep.equal(['tag3-a'])
|
|
420
|
+
|
|
421
|
+
// Remove one instance
|
|
422
|
+
inputTag2.remove()
|
|
423
|
+
await waitForUpdate()
|
|
424
|
+
|
|
425
|
+
// Others should still work
|
|
426
|
+
inputTag1.add('tag1-b')
|
|
427
|
+
inputTag3.add('tag3-b')
|
|
428
|
+
await waitForUpdate()
|
|
429
|
+
|
|
430
|
+
expect(getTagValues(inputTag1)).to.deep.equal(['tag1-a', 'tag1-b'])
|
|
431
|
+
expect(getTagValues(inputTag3)).to.deep.equal(['tag3-a', 'tag3-b'])
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
describe('Invalid HTML Scenarios', () => {
|
|
436
|
+
it('should handle malformed tag-option elements', async () => {
|
|
437
|
+
document.body.innerHTML = `
|
|
438
|
+
<input-tag name="tags" multiple>
|
|
439
|
+
<tag-option value="good">Good Tag</tag-option>
|
|
440
|
+
<tag-option>No Value Attribute</tag-option>
|
|
441
|
+
<tag-option value="">Empty Value</tag-option>
|
|
442
|
+
<tag-option value="valid">Valid Tag</tag-option>
|
|
443
|
+
</input-tag>
|
|
444
|
+
`
|
|
445
|
+
|
|
446
|
+
const inputTag = document.querySelector('input-tag')
|
|
447
|
+
await waitForBasicInitialization(inputTag)
|
|
448
|
+
|
|
449
|
+
const values = getTagValues(inputTag)
|
|
450
|
+
expect(values).to.include('good')
|
|
451
|
+
expect(values).to.include('valid')
|
|
452
|
+
|
|
453
|
+
// Should handle the malformed ones gracefully
|
|
454
|
+
expect(values.length).to.be.greaterThan(0)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('should handle nested HTML content in tag-options', async () => {
|
|
458
|
+
document.body.innerHTML = `
|
|
459
|
+
<input-tag name="tags" multiple>
|
|
460
|
+
<tag-option value="simple">Simple</tag-option>
|
|
461
|
+
<tag-option value="with-html">
|
|
462
|
+
<strong>Bold</strong> content
|
|
463
|
+
</tag-option>
|
|
464
|
+
<tag-option value="with-nested">
|
|
465
|
+
<span>Nested <em>elements</em></span>
|
|
466
|
+
</tag-option>
|
|
467
|
+
</input-tag>
|
|
468
|
+
`
|
|
469
|
+
|
|
470
|
+
const inputTag = document.querySelector('input-tag')
|
|
471
|
+
await waitForBasicInitialization(inputTag)
|
|
472
|
+
|
|
473
|
+
expect(getTagElements(inputTag)).to.have.length(3)
|
|
474
|
+
expect(getTagValues(inputTag)).to.deep.equal(['simple', 'with-html', 'with-nested'])
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('should handle input-tag with no name attribute', async () => {
|
|
478
|
+
const inputTag = await setupInputTag('<input-tag multiple></input-tag>')
|
|
479
|
+
|
|
480
|
+
expect(inputTag.name).to.be.null
|
|
481
|
+
|
|
482
|
+
// Should still work for basic functionality
|
|
483
|
+
inputTag.add('unnamed-tag')
|
|
484
|
+
await waitForUpdate()
|
|
485
|
+
|
|
486
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
487
|
+
expect(getTagValues(inputTag)).to.deep.equal(['unnamed-tag'])
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
describe('Android Comma Support', () => {
|
|
492
|
+
it('should handle Android keyboard comma behavior', async () => {
|
|
493
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
494
|
+
const input = inputTag._taggleInputTarget
|
|
495
|
+
|
|
496
|
+
// Simulate Android keyboard behavior (keyCode 229)
|
|
497
|
+
await simulateInput(input, 'android-tag,')
|
|
498
|
+
simulateKeyup(input, 229) // Android composite keycode
|
|
499
|
+
|
|
500
|
+
await waitForUpdate()
|
|
501
|
+
|
|
502
|
+
expect(getTagElements(inputTag)).to.have.length(1)
|
|
503
|
+
expect(getTagValues(inputTag)).to.deep.equal(['android-tag'])
|
|
504
|
+
expect(input.value).to.equal('')
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('should handle Android backspace behavior', async () => {
|
|
508
|
+
const inputTag = await setupInputTag(`
|
|
509
|
+
<input-tag name="tags" multiple>
|
|
510
|
+
<tag-option value="android-existing">Android Existing</tag-option>
|
|
511
|
+
</input-tag>
|
|
512
|
+
`)
|
|
513
|
+
const input = inputTag._taggleInputTarget
|
|
514
|
+
|
|
515
|
+
// Simulate Android backspace with empty input
|
|
516
|
+
await simulateInput(input, '')
|
|
517
|
+
simulateKeyup(input, 229)
|
|
518
|
+
|
|
519
|
+
await waitForUpdate()
|
|
520
|
+
|
|
521
|
+
expect(getTagElements(inputTag)).to.have.length(0)
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
})
|