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.
@@ -0,0 +1,567 @@
1
+ import { expect } from '@esm-bundle/chai'
2
+ import '../src/input-tag.js'
3
+ import {
4
+ setupGlobalTestHooks,
5
+ setupInputTag,
6
+ waitForUpdate,
7
+ simulateKeydown,
8
+ simulateInput,
9
+ simulateClick,
10
+ simulateUserInput,
11
+ simulateUserAddTag,
12
+ simulateUserAddTagWithKey,
13
+ clickTagRemoveButton,
14
+ getTagElements,
15
+ getTagValues,
16
+ KEYCODES
17
+ } from './lib/test-utils.js'
18
+
19
+ describe('Basic Tag Functionality', () => {
20
+ setupGlobalTestHooks()
21
+
22
+ describe('Single Mode Input Visibility', () => {
23
+ it('should hide input and button when tag exists in single mode', async () => {
24
+ const inputTag = await setupInputTag(`
25
+ <input-tag name="status">
26
+ <tag-option value="active">Active</tag-option>
27
+ </input-tag>
28
+ `)
29
+
30
+ // Input and button should be hidden when tag exists
31
+ const input = inputTag._taggleInputTarget
32
+ const button = inputTag.buttonTarget
33
+
34
+ expect(input.style.display).to.equal('none')
35
+ expect(button.style.display).to.equal('none')
36
+ })
37
+
38
+ it('should show input and button when no tag exists in single mode', async () => {
39
+ const inputTag = await setupInputTag('<input-tag name="status"></input-tag>')
40
+
41
+ // Input and button should be visible when no tag exists
42
+ const input = inputTag._taggleInputTarget
43
+ const button = inputTag.buttonTarget
44
+
45
+ expect(input.style.display).to.not.equal('none')
46
+ expect(button.style.display).to.not.equal('none')
47
+ })
48
+
49
+ it('should show input and button after removing tag in single mode', async () => {
50
+ const inputTag = await setupInputTag(`
51
+ <input-tag name="status">
52
+ <tag-option value="active">Active</tag-option>
53
+ </input-tag>
54
+ `)
55
+
56
+ // Remove the tag
57
+ const tagElement = getTagElements(inputTag)[0]
58
+ clickTagRemoveButton(tagElement)
59
+ await waitForUpdate()
60
+
61
+ // Input and button should be visible again
62
+ const input = inputTag._taggleInputTarget
63
+ const button = inputTag.buttonTarget
64
+
65
+ expect(input.style.display).to.not.equal('none')
66
+ expect(button.style.display).to.not.equal('none')
67
+ })
68
+
69
+ it('should hide input and button after adding tag in single mode', async () => {
70
+ const inputTag = await setupInputTag('<input-tag name="status"></input-tag>')
71
+
72
+ // Add a tag
73
+ await simulateUserAddTag(inputTag, 'active')
74
+
75
+ // Input and button should be hidden now
76
+ const input = inputTag._taggleInputTarget
77
+ const button = inputTag.buttonTarget
78
+
79
+ expect(input.style.display).to.equal('none')
80
+ expect(button.style.display).to.equal('none')
81
+ })
82
+
83
+ it('should always show input and button in multiple mode', async () => {
84
+ const inputTag = await setupInputTag(`
85
+ <input-tag name="tags" multiple>
86
+ <tag-option value="tag1">Tag 1</tag-option>
87
+ <tag-option value="tag2">Tag 2</tag-option>
88
+ </input-tag>
89
+ `)
90
+
91
+ // Input and button should always be visible in multiple mode
92
+ const input = inputTag._taggleInputTarget
93
+ const button = inputTag.buttonTarget
94
+
95
+ expect(input.style.display).to.not.equal('none')
96
+ expect(button.style.display).to.not.equal('none')
97
+ })
98
+ })
99
+
100
+ describe('Empty Form Serialization', () => {
101
+ it('should serialize empty multiple input-tag with empty string so server knows to clear values', async () => {
102
+ const form = document.createElement('form')
103
+ form.innerHTML = `<input-tag name="tags" multiple></input-tag>`
104
+ document.body.appendChild(form)
105
+
106
+ const inputTag = form.querySelector('input-tag')
107
+ await waitForUpdate()
108
+
109
+ // Wait for component initialization
110
+ while (!inputTag._taggle) {
111
+ await waitForUpdate()
112
+ }
113
+
114
+ const formData = new FormData(form)
115
+ const values = formData.getAll('tags')
116
+ // Should include empty string so server knows to clear values (like Rails multiple checkboxes)
117
+ expect(values).to.deep.equal([''])
118
+
119
+ document.body.removeChild(form)
120
+ })
121
+
122
+ it('should serialize empty single input-tag as empty string (like standard HTML inputs)', async () => {
123
+ const form = document.createElement('form')
124
+ form.innerHTML = `<input-tag name="status"></input-tag>`
125
+ document.body.appendChild(form)
126
+
127
+ const inputTag = form.querySelector('input-tag')
128
+ await waitForUpdate()
129
+
130
+ // Wait for component initialization
131
+ while (!inputTag._taggle) {
132
+ await waitForUpdate()
133
+ }
134
+
135
+ const formData = new FormData(form)
136
+ const value = formData.get('status')
137
+ expect(value).to.equal('')
138
+
139
+ document.body.removeChild(form)
140
+ })
141
+
142
+ it('should include empty string in form data for empty multiple input-tag', async () => {
143
+ const form = document.createElement('form')
144
+ form.innerHTML = `<input-tag name="tags" multiple></input-tag>`
145
+ document.body.appendChild(form)
146
+
147
+ const inputTag = form.querySelector('input-tag')
148
+ await waitForUpdate()
149
+
150
+ // Wait for component initialization
151
+ while (!inputTag._taggle) {
152
+ await waitForUpdate()
153
+ }
154
+
155
+ const formData = new FormData(form)
156
+ const values = formData.getAll('tags')
157
+ // Should include empty string so server knows to clear values (like Rails multiple checkboxes)
158
+ expect(values).to.include('')
159
+
160
+ document.body.removeChild(form)
161
+ })
162
+
163
+ it('should include empty string in form data for empty single input-tag (like standard HTML inputs)', async () => {
164
+ const form = document.createElement('form')
165
+ form.innerHTML = `<input-tag name="status"></input-tag>`
166
+ document.body.appendChild(form)
167
+
168
+ const inputTag = form.querySelector('input-tag')
169
+ await waitForUpdate()
170
+
171
+ // Wait for component initialization
172
+ while (!inputTag._taggle) {
173
+ await waitForUpdate()
174
+ }
175
+
176
+ const formData = new FormData(form)
177
+ const values = formData.getAll('status')
178
+ expect(values).to.deep.equal([''])
179
+
180
+ document.body.removeChild(form)
181
+ })
182
+ })
183
+
184
+ describe('Tag Creation', () => {
185
+ it('should create empty input-tag', async () => {
186
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
187
+ expect(getTagElements(inputTag)).to.have.length(0)
188
+ expect(inputTag.tags).to.deep.equal([])
189
+ })
190
+
191
+ it('should initialize with pre-existing tag-option elements', async () => {
192
+ const inputTag = await setupInputTag(`
193
+ <input-tag name="tags" multiple>
194
+ <tag-option value="javascript">JavaScript</tag-option>
195
+ <tag-option value="python">Python</tag-option>
196
+ <tag-option value="ruby">Ruby</tag-option>
197
+ </input-tag>
198
+ `)
199
+
200
+ expect(getTagElements(inputTag)).to.have.length(3)
201
+ expect(getTagValues(inputTag)).to.deep.equal(['javascript', 'python', 'ruby'])
202
+ expect(inputTag.tags).to.deep.equal(['javascript', 'python', 'ruby'])
203
+ })
204
+
205
+ it('should add tags programmatically via .add()', async () => {
206
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
207
+
208
+ inputTag.add('react')
209
+ await waitForUpdate()
210
+
211
+ expect(getTagElements(inputTag)).to.have.length(1)
212
+ expect(getTagValues(inputTag)).to.deep.equal(['react'])
213
+ })
214
+
215
+ it('should add multiple tags at once', async () => {
216
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
217
+
218
+ inputTag.add(['vue', 'angular', 'svelte'])
219
+ await waitForUpdate()
220
+
221
+ expect(getTagElements(inputTag)).to.have.length(3)
222
+ expect(getTagValues(inputTag)).to.deep.equal(['vue', 'angular', 'svelte'])
223
+ })
224
+
225
+ it('should add tags via Enter key', async () => {
226
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
227
+
228
+ await simulateUserAddTag(inputTag, 'typescript')
229
+
230
+ expect(getTagElements(inputTag)).to.have.length(1)
231
+ expect(getTagValues(inputTag)).to.deep.equal(['typescript'])
232
+ expect(inputTag.tags).to.deep.equal(['typescript'])
233
+ })
234
+
235
+ it('should add tags via Tab key', async () => {
236
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
237
+
238
+ await simulateUserAddTagWithKey(inputTag, 'css', KEYCODES.TAB)
239
+
240
+ expect(getTagElements(inputTag)).to.have.length(1)
241
+ expect(getTagValues(inputTag)).to.deep.equal(['css'])
242
+ expect(inputTag.tags).to.deep.equal(['css'])
243
+ })
244
+
245
+ it('should add tags via Comma key', async () => {
246
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
247
+
248
+ await simulateUserAddTagWithKey(inputTag, 'html', KEYCODES.COMMA)
249
+
250
+ expect(getTagElements(inputTag)).to.have.length(1)
251
+ expect(getTagValues(inputTag)).to.deep.equal(['html'])
252
+ expect(inputTag.tags).to.deep.equal(['html'])
253
+ })
254
+
255
+ it('should add tags via + button', async () => {
256
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
257
+
258
+ // Type in the input
259
+ simulateUserInput(inputTag, 'sass')
260
+
261
+ // Click the + button (find it via shadow DOM)
262
+ const button = inputTag.shadowRoot.querySelector('button.add')
263
+ simulateClick(button)
264
+ await waitForUpdate()
265
+
266
+ expect(getTagElements(inputTag)).to.have.length(1)
267
+ expect(getTagValues(inputTag)).to.deep.equal(['sass'])
268
+ expect(inputTag.tags).to.deep.equal(['sass'])
269
+ })
270
+
271
+ it('should handle comma-separated input', async () => {
272
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
273
+
274
+ inputTag.add('react,vue,angular')
275
+ await waitForUpdate()
276
+
277
+ expect(getTagElements(inputTag)).to.have.length(3)
278
+ expect(getTagValues(inputTag)).to.deep.equal(['react', 'vue', 'angular'])
279
+ })
280
+ })
281
+
282
+ describe('Tag Removal', () => {
283
+ it('should remove tags by clicking X button', async () => {
284
+ const inputTag = await setupInputTag(`
285
+ <input-tag name="tags" multiple>
286
+ <tag-option value="remove-me">Remove Me</tag-option>
287
+ <tag-option value="keep-me">Keep Me</tag-option>
288
+ </input-tag>
289
+ `)
290
+
291
+ const tagToRemove = getTagElements(inputTag)[0]
292
+ clickTagRemoveButton(tagToRemove)
293
+ await waitForUpdate()
294
+
295
+ expect(getTagElements(inputTag)).to.have.length(1)
296
+ expect(getTagValues(inputTag)).to.deep.equal(['keep-me'])
297
+ })
298
+
299
+ it('should remove tags programmatically via .remove()', async () => {
300
+ const inputTag = await setupInputTag(`
301
+ <input-tag name="tags" multiple>
302
+ <tag-option value="first">First</tag-option>
303
+ <tag-option value="second">Second</tag-option>
304
+ </input-tag>
305
+ `)
306
+
307
+ inputTag.remove('first')
308
+ await waitForUpdate()
309
+
310
+ expect(getTagElements(inputTag)).to.have.length(1)
311
+ expect(getTagValues(inputTag)).to.deep.equal(['second'])
312
+ })
313
+
314
+ it('should remove last tag via backspace when input is empty', async () => {
315
+ const inputTag = await setupInputTag(`
316
+ <input-tag name="tags" multiple>
317
+ <tag-option value="will-be-removed">Will Be Removed</tag-option>
318
+ </input-tag>
319
+ `)
320
+
321
+ // Focus the input-tag and simulate backspace behavior
322
+ inputTag.focus()
323
+ const input = simulateUserInput(inputTag, '') // Empty input to trigger backspace behavior
324
+
325
+ // First backspace highlights the tag
326
+ if (input) {
327
+ simulateKeydown(input, KEYCODES.BACKSPACE)
328
+ }
329
+ await waitForUpdate()
330
+
331
+ // Second backspace removes it
332
+ if (input) {
333
+ simulateKeydown(input, KEYCODES.BACKSPACE)
334
+ }
335
+ await waitForUpdate()
336
+
337
+ expect(getTagElements(inputTag)).to.have.length(0)
338
+ })
339
+
340
+ it('should remove all tags via .removeAll()', async () => {
341
+ const inputTag = await setupInputTag(`
342
+ <input-tag name="tags" multiple>
343
+ <tag-option value="first">First</tag-option>
344
+ <tag-option value="second">Second</tag-option>
345
+ <tag-option value="third">Third</tag-option>
346
+ </input-tag>
347
+ `)
348
+
349
+ inputTag.removeAll()
350
+ await waitForUpdate()
351
+
352
+ expect(getTagElements(inputTag)).to.have.length(0)
353
+ expect(inputTag.tags).to.deep.equal([])
354
+ })
355
+
356
+ it('should remove tags via Delete key navigation', async () => {
357
+ const inputTag = await setupInputTag(`
358
+ <input-tag name="tags" multiple>
359
+ <tag-option value="first">First</tag-option>
360
+ <tag-option value="second">Second</tag-option>
361
+ </input-tag>
362
+ `)
363
+
364
+ // Focus and simulate delete key behavior
365
+ const input = simulateUserInput(inputTag, '') // Focus and get internal input
366
+
367
+ // Delete key should remove next tag
368
+ if (input) {
369
+ simulateKeydown(input, KEYCODES.DELETE)
370
+ await waitForUpdate()
371
+ simulateKeydown(input, KEYCODES.DELETE)
372
+ await waitForUpdate()
373
+ }
374
+
375
+ expect(getTagElements(inputTag)).to.have.length(1)
376
+ })
377
+ })
378
+
379
+ describe('Single vs Multiple Mode', () => {
380
+ it('should allow multiple tags when multiple attribute is present', async () => {
381
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
382
+
383
+ inputTag.add('first')
384
+ inputTag.add('second')
385
+ inputTag.add('third')
386
+ await waitForUpdate()
387
+
388
+ expect(getTagElements(inputTag)).to.have.length(3)
389
+ expect(getTagValues(inputTag)).to.deep.equal(['first', 'second', 'third'])
390
+ })
391
+
392
+ it('should only allow one tag when multiple attribute is not present', async () => {
393
+ const inputTag = await setupInputTag('<input-tag name="tag"></input-tag>')
394
+
395
+ inputTag.add('first')
396
+ await waitForUpdate()
397
+ expect(getTagElements(inputTag)).to.have.length(1)
398
+
399
+ // Try to add second tag - should not be added
400
+ inputTag.add('second')
401
+ await waitForUpdate()
402
+
403
+ expect(getTagElements(inputTag)).to.have.length(1)
404
+ expect(getTagValues(inputTag)).to.deep.equal(['first'])
405
+ })
406
+
407
+ it('should replace existing tag in single mode', async () => {
408
+ const inputTag = await setupInputTag(`
409
+ <input-tag name="tag">
410
+ <tag-option value="original">Original</tag-option>
411
+ </input-tag>
412
+ `)
413
+
414
+ // Remove existing and add new (simulating replacement)
415
+ inputTag.removeAll()
416
+ inputTag.add('replacement')
417
+ await waitForUpdate()
418
+
419
+ expect(getTagElements(inputTag)).to.have.length(1)
420
+ expect(getTagValues(inputTag)).to.deep.equal(['replacement'])
421
+ })
422
+ })
423
+
424
+ describe('Tag Display and Styling', () => {
425
+ it('should render tag-option elements with shadow DOM', async () => {
426
+ const inputTag = await setupInputTag(`
427
+ <input-tag name="tags" multiple>
428
+ <tag-option value="test">Test Tag</tag-option>
429
+ </input-tag>
430
+ `)
431
+
432
+ const tagElement = getTagElements(inputTag)[0]
433
+ expect(tagElement.shadowRoot).to.not.be.null
434
+ expect(tagElement.shadowRoot.querySelector('button')).to.not.be.null
435
+ expect(tagElement.shadowRoot.querySelector('slot')).to.not.be.null
436
+ })
437
+
438
+ it('should show tag text content', async () => {
439
+ const inputTag = await setupInputTag(`
440
+ <input-tag name="tags" multiple>
441
+ <tag-option value="js">JavaScript</tag-option>
442
+ </input-tag>
443
+ `)
444
+
445
+ const tagElement = getTagElements(inputTag)[0]
446
+ expect(tagElement.textContent.trim()).to.equal('JavaScript')
447
+ expect(tagElement.value).to.equal('js')
448
+ })
449
+
450
+ it('should use value attribute if present, otherwise use text content', async () => {
451
+ const inputTag = await setupInputTag(`
452
+ <input-tag name="tags" multiple>
453
+ <tag-option value="explicit-value">Display Text</tag-option>
454
+ <tag-option>Text Only</tag-option>
455
+ </input-tag>
456
+ `)
457
+
458
+ const tags = getTagElements(inputTag)
459
+ expect(tags[0].value).to.equal('explicit-value')
460
+ expect(tags[1].value).to.equal('Text Only')
461
+ })
462
+ })
463
+
464
+ describe('Tag Validation and Restrictions', () => {
465
+ it('should not add empty tags', async () => {
466
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
467
+
468
+ // Try to add empty tag
469
+ await simulateUserAddTag(inputTag, '')
470
+
471
+ expect(getTagElements(inputTag)).to.have.length(0)
472
+ expect(inputTag.tags).to.deep.equal([])
473
+ })
474
+
475
+ it('should trim whitespace from tags', async () => {
476
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
477
+
478
+ inputTag.add(' spaced ')
479
+ await waitForUpdate()
480
+
481
+ expect(getTagValues(inputTag)).to.deep.equal(['spaced'])
482
+ })
483
+
484
+ it('should prevent duplicate tags by default', async () => {
485
+ const inputTag = await setupInputTag(`
486
+ <input-tag name="tags" multiple>
487
+ <tag-option value="existing">Existing</tag-option>
488
+ </input-tag>
489
+ `)
490
+
491
+ inputTag.add('existing')
492
+ await waitForUpdate()
493
+
494
+ expect(getTagElements(inputTag)).to.have.length(1)
495
+ expect(getTagValues(inputTag)).to.deep.equal(['existing'])
496
+ })
497
+
498
+ it('should handle case sensitivity in duplicates', async () => {
499
+ const inputTag = await setupInputTag(`
500
+ <input-tag name="tags" multiple>
501
+ <tag-option value="javascript">JavaScript</tag-option>
502
+ </input-tag>
503
+ `)
504
+
505
+ // Case-sensitive duplicates are allowed (different case = different tag)
506
+ inputTag.add('JavaScript')
507
+ await waitForUpdate()
508
+
509
+ expect(getTagElements(inputTag)).to.have.length(2)
510
+ expect(getTagValues(inputTag)).to.deep.equal(['javascript', 'JavaScript'])
511
+
512
+ // Exact duplicates should be prevented
513
+ inputTag.add('javascript')
514
+ await waitForUpdate()
515
+
516
+ expect(getTagElements(inputTag)).to.have.length(2) // Still 2, no new tag added
517
+ expect(getTagValues(inputTag)).to.deep.equal(['javascript', 'JavaScript'])
518
+ })
519
+ })
520
+
521
+ describe('Tag Ordering and Positioning', () => {
522
+ it('should maintain tag order when adding', async () => {
523
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
524
+
525
+ inputTag.add('first')
526
+ inputTag.add('second')
527
+ inputTag.add('third')
528
+ await waitForUpdate()
529
+
530
+ expect(getTagValues(inputTag)).to.deep.equal(['first', 'second', 'third'])
531
+ })
532
+
533
+ it('should maintain tag order when removing from middle', async () => {
534
+ const inputTag = await setupInputTag(`
535
+ <input-tag name="tags" multiple>
536
+ <tag-option value="first">First</tag-option>
537
+ <tag-option value="middle">Middle</tag-option>
538
+ <tag-option value="last">Last</tag-option>
539
+ </input-tag>
540
+ `)
541
+
542
+ inputTag.remove('middle')
543
+ await waitForUpdate()
544
+
545
+ expect(getTagValues(inputTag)).to.deep.equal(['first', 'last'])
546
+ })
547
+
548
+ it('should add new tags after existing ones (positional insertion not supported in DOM)', async () => {
549
+ const inputTag = await setupInputTag(`
550
+ <input-tag name="tags" multiple>
551
+ <tag-option value="first">First</tag-option>
552
+ <tag-option value="third">Third</tag-option>
553
+ </input-tag>
554
+ `)
555
+
556
+ // Add a tag (positional insertion doesn't update DOM order)
557
+ inputTag.addAt('second', 1)
558
+ await waitForUpdate()
559
+
560
+ // New tags are added to the DOM in the order they're created
561
+ expect(getTagValues(inputTag)).to.deep.equal(['first', 'third', 'second'])
562
+
563
+ // But internal order might be different - verify all tags
564
+ expect(inputTag.tags).to.include.members(['first', 'third', 'second'])
565
+ })
566
+ })
567
+ })