bard-tag_field 0.5.1 → 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.
@@ -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
+ })