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,466 @@
1
+ import { expect } from '@esm-bundle/chai'
2
+ import '../src/input-tag.js'
3
+ import {
4
+ setupGlobalTestHooks,
5
+ waitForElement,
6
+ waitForUpdate,
7
+ getTagElements,
8
+ getTagValues
9
+ } from './lib/test-utils.js'
10
+
11
+ describe('DOM Mutation Handling', () => {
12
+ setupGlobalTestHooks()
13
+
14
+ describe('Attribute Changes', () => {
15
+ it('should update when name attribute changes', async () => {
16
+ document.body.innerHTML = `
17
+ <input-tag name="original-name" multiple>
18
+ <tag-option value="test">Test</tag-option>
19
+ </input-tag>
20
+ `
21
+ const inputTag = document.querySelector('input-tag')
22
+ await waitForElement(inputTag, '_taggle')
23
+
24
+ expect(inputTag.name).to.equal('original-name')
25
+
26
+ // Simulate DOM morphing changing the name attribute
27
+ inputTag.setAttribute('name', 'morphed-name')
28
+ await waitForUpdate()
29
+
30
+ expect(inputTag.name).to.equal('morphed-name')
31
+
32
+ // Check that internal hidden input also updated
33
+ const hiddenInput = inputTag.shadowRoot.querySelector('input[type="hidden"]')
34
+ expect(hiddenInput.name).to.equal('morphed-name')
35
+ })
36
+
37
+ it('should update when multiple attribute is added', async () => {
38
+ document.body.innerHTML = `
39
+ <input-tag name="tags">
40
+ <tag-option value="single">Single</tag-option>
41
+ </input-tag>
42
+ `
43
+ const inputTag = document.querySelector('input-tag')
44
+ await waitForElement(inputTag, '_taggle')
45
+
46
+ expect(getTagElements(inputTag)).to.have.length(1)
47
+
48
+ // Add multiple attribute via DOM mutation
49
+ inputTag.setAttribute('multiple', '')
50
+ await waitForUpdate()
51
+
52
+ // Should be able to add more tags now
53
+ inputTag.add('second')
54
+ inputTag.add('third')
55
+ await waitForUpdate()
56
+
57
+ expect(getTagElements(inputTag)).to.have.length(3)
58
+ expect(getTagValues(inputTag)).to.deep.equal(['single', 'second', 'third'])
59
+ })
60
+
61
+ it('should update when multiple attribute is removed', async () => {
62
+ document.body.innerHTML = `
63
+ <input-tag name="tags" multiple>
64
+ <tag-option value="first">First</tag-option>
65
+ <tag-option value="second">Second</tag-option>
66
+ </input-tag>
67
+ `
68
+ const inputTag = document.querySelector('input-tag')
69
+ await waitForElement(inputTag, '_taggle')
70
+
71
+ expect(getTagElements(inputTag)).to.have.length(2)
72
+
73
+ // Remove multiple attribute via DOM mutation
74
+ inputTag.removeAttribute('multiple')
75
+ await waitForUpdate()
76
+
77
+ // Should now be limited to single tag mode
78
+ expect(getTagElements(inputTag)).to.have.length(1)
79
+
80
+ // Trying to add another should not work
81
+ inputTag.add('third')
82
+ await waitForUpdate()
83
+ expect(getTagElements(inputTag)).to.have.length(1)
84
+ })
85
+
86
+ it('should update when required attribute changes', async () => {
87
+ document.body.innerHTML = `
88
+ <input-tag name="tags" multiple></input-tag>
89
+ `
90
+ const inputTag = document.querySelector('input-tag')
91
+ await waitForElement(inputTag, '_taggle')
92
+
93
+ expect(inputTag.checkValidity()).to.be.true
94
+
95
+ // Add required attribute via DOM mutation
96
+ inputTag.setAttribute('required', '')
97
+ await waitForUpdate()
98
+
99
+ expect(inputTag.checkValidity()).to.be.false
100
+
101
+ // Add a tag to make it valid
102
+ inputTag.add('required-tag')
103
+ await waitForUpdate()
104
+
105
+ expect(inputTag.checkValidity()).to.be.true
106
+ })
107
+
108
+ it('should update when list attribute changes', async () => {
109
+ document.body.innerHTML = `
110
+ <input-tag name="tags" multiple></input-tag>
111
+ <datalist id="options1">
112
+ <option value="option1">Option 1</option>
113
+ </datalist>
114
+ <datalist id="options2">
115
+ <option value="option2">Option 2</option>
116
+ <option value="option3">Option 3</option>
117
+ </datalist>
118
+ `
119
+ const inputTag = document.querySelector('input-tag')
120
+ await waitForElement(inputTag, '_taggle')
121
+
122
+ expect(inputTag.options).to.deep.equal([])
123
+
124
+ // Add list attribute via DOM mutation
125
+ inputTag.setAttribute('list', 'options1')
126
+ await waitForUpdate()
127
+
128
+ expect(inputTag.options).to.deep.equal(['option1'])
129
+
130
+ // Change list attribute to different datalist
131
+ inputTag.setAttribute('list', 'options2')
132
+ await waitForUpdate()
133
+
134
+ expect(inputTag.options).to.deep.equal(['option2', 'option3'])
135
+ })
136
+ })
137
+
138
+ describe('Tag Option Changes', () => {
139
+ it('should update when tag-option elements are added', async () => {
140
+ document.body.innerHTML = `
141
+ <input-tag name="tags" multiple>
142
+ <tag-option value="initial">Initial</tag-option>
143
+ </input-tag>
144
+ `
145
+ const inputTag = document.querySelector('input-tag')
146
+ await waitForElement(inputTag, '_taggle')
147
+
148
+ expect(getTagElements(inputTag)).to.have.length(1)
149
+ expect(getTagValues(inputTag)).to.deep.equal(['initial'])
150
+
151
+ // Add new tag-option via DOM mutation
152
+ const newTagOption = document.createElement('tag-option')
153
+ newTagOption.setAttribute('value', 'added')
154
+ newTagOption.textContent = 'Added'
155
+ inputTag.appendChild(newTagOption)
156
+ await waitForUpdate()
157
+
158
+ expect(getTagElements(inputTag)).to.have.length(2)
159
+ expect(getTagValues(inputTag)).to.deep.equal(['initial', 'added'])
160
+ })
161
+
162
+ it('should update when tag-option elements are removed', async () => {
163
+ document.body.innerHTML = `
164
+ <input-tag name="tags" multiple>
165
+ <tag-option value="keep">Keep</tag-option>
166
+ <tag-option value="remove">Remove</tag-option>
167
+ <tag-option value="also-keep">Also Keep</tag-option>
168
+ </input-tag>
169
+ `
170
+ const inputTag = document.querySelector('input-tag')
171
+ await waitForElement(inputTag, '_taggle')
172
+
173
+ expect(getTagElements(inputTag)).to.have.length(3)
174
+ expect(getTagValues(inputTag)).to.deep.equal(['keep', 'remove', 'also-keep'])
175
+
176
+ // Remove tag-option via DOM mutation
177
+ const tagOptionToRemove = inputTag.querySelector('tag-option[value="remove"]')
178
+ inputTag.removeChild(tagOptionToRemove)
179
+ await waitForUpdate()
180
+
181
+ expect(getTagElements(inputTag)).to.have.length(2)
182
+ expect(getTagValues(inputTag)).to.deep.equal(['keep', 'also-keep'])
183
+ })
184
+
185
+ it('should update when tag-option value attribute changes', async () => {
186
+ document.body.innerHTML = `
187
+ <input-tag name="tags" multiple>
188
+ <tag-option value="original">Original</tag-option>
189
+ </input-tag>
190
+ `
191
+ const inputTag = document.querySelector('input-tag')
192
+ await waitForElement(inputTag, '_taggle')
193
+
194
+ expect(getTagValues(inputTag)).to.deep.equal(['original'])
195
+
196
+ // Change tag-option value via DOM mutation
197
+ const tagOption = inputTag.querySelector('tag-option')
198
+ tagOption.setAttribute('value', 'modified')
199
+ await waitForUpdate()
200
+
201
+ expect(getTagValues(inputTag)).to.deep.equal(['modified'])
202
+
203
+ // Verify the tag visually updated too
204
+ const tagElements = getTagElements(inputTag)
205
+ expect(tagElements[0].getAttribute('data-value')).to.equal('modified')
206
+ })
207
+
208
+ it('should update when tag-option text content changes', async () => {
209
+ document.body.innerHTML = `
210
+ <input-tag name="tags" multiple>
211
+ <tag-option value="test">Original Text</tag-option>
212
+ </input-tag>
213
+ `
214
+ const inputTag = document.querySelector('input-tag')
215
+ await waitForElement(inputTag, '_taggle')
216
+
217
+ // Change tag-option text content via DOM mutation
218
+ const tagOption = inputTag.querySelector('tag-option')
219
+ tagOption.textContent = 'Modified Text'
220
+ await waitForUpdate()
221
+
222
+ // Verify the tag display updated
223
+ const tagElements = getTagElements(inputTag)
224
+ expect(tagElements[0].textContent.trim()).to.include('Modified Text')
225
+ })
226
+ })
227
+
228
+ describe('Complex DOM Mutations', () => {
229
+ it('should handle multiple simultaneous attribute changes', async () => {
230
+ document.body.innerHTML = `
231
+ <input-tag name="original" multiple>
232
+ <tag-option value="test">Test</tag-option>
233
+ </input-tag>
234
+ <datalist id="new-options">
235
+ <option value="option1">Option 1</option>
236
+ </datalist>
237
+ `
238
+ const inputTag = document.querySelector('input-tag')
239
+ await waitForElement(inputTag, '_taggle')
240
+
241
+ // Simulate complex DOM morphing changing multiple attributes
242
+ inputTag.setAttribute('name', 'morphed')
243
+ inputTag.setAttribute('required', '')
244
+ inputTag.setAttribute('list', 'new-options')
245
+ await waitForUpdate()
246
+
247
+ expect(inputTag.name).to.equal('morphed')
248
+ expect(inputTag.hasAttribute('required')).to.be.true
249
+ expect(inputTag.options).to.deep.equal(['option1'])
250
+
251
+ // Should still be invalid due to having no valid tags for required field
252
+ expect(inputTag.checkValidity()).to.be.true // has existing tag
253
+ })
254
+
255
+ it('should handle tag-option replacement via innerHTML', async () => {
256
+ document.body.innerHTML = `
257
+ <input-tag name="tags" multiple>
258
+ <tag-option value="old1">Old 1</tag-option>
259
+ <tag-option value="old2">Old 2</tag-option>
260
+ </input-tag>
261
+ `
262
+ const inputTag = document.querySelector('input-tag')
263
+ await waitForElement(inputTag, '_taggle')
264
+
265
+ expect(getTagValues(inputTag)).to.deep.equal(['old1', 'old2'])
266
+
267
+ // Replace all content via innerHTML (simulating morphing)
268
+ inputTag.innerHTML = `
269
+ <tag-option value="new1">New 1</tag-option>
270
+ <tag-option value="new2">New 2</tag-option>
271
+ <tag-option value="new3">New 3</tag-option>
272
+ `
273
+ await waitForUpdate()
274
+
275
+ expect(getTagValues(inputTag)).to.deep.equal(['new1', 'new2', 'new3'])
276
+ expect(getTagElements(inputTag)).to.have.length(3)
277
+ })
278
+
279
+ it('should handle mixed tag-option additions and removals', async () => {
280
+ document.body.innerHTML = `
281
+ <input-tag name="tags" multiple>
282
+ <tag-option value="keep">Keep</tag-option>
283
+ <tag-option value="remove">Remove</tag-option>
284
+ </input-tag>
285
+ `
286
+ const inputTag = document.querySelector('input-tag')
287
+ await waitForElement(inputTag, '_taggle')
288
+
289
+ expect(getTagValues(inputTag)).to.deep.equal(['keep', 'remove'])
290
+
291
+ // Remove one and add two new ones
292
+ const removeOption = inputTag.querySelector('tag-option[value="remove"]')
293
+ inputTag.removeChild(removeOption)
294
+
295
+ const newOption1 = document.createElement('tag-option')
296
+ newOption1.setAttribute('value', 'new1')
297
+ newOption1.textContent = 'New 1'
298
+ inputTag.appendChild(newOption1)
299
+
300
+ const newOption2 = document.createElement('tag-option')
301
+ newOption2.setAttribute('value', 'new2')
302
+ newOption2.textContent = 'New 2'
303
+ inputTag.appendChild(newOption2)
304
+
305
+ await waitForUpdate()
306
+
307
+ expect(getTagValues(inputTag)).to.deep.equal(['keep', 'new1', 'new2'])
308
+ expect(getTagElements(inputTag)).to.have.length(3)
309
+ })
310
+ })
311
+
312
+ describe('MutationObserver Integration', () => {
313
+ it('should detect attribute mutations via MutationObserver', async () => {
314
+ document.body.innerHTML = `
315
+ <input-tag name="observer-test" multiple>
316
+ <tag-option value="test">Test</tag-option>
317
+ </input-tag>
318
+ `
319
+ const inputTag = document.querySelector('input-tag')
320
+ await waitForElement(inputTag, '_taggle')
321
+
322
+ // Verify MutationObserver is active
323
+ expect(inputTag.observer).to.not.be.null
324
+
325
+ // Make attribute change that should be observed
326
+ inputTag.setAttribute('name', 'observed-change')
327
+ await waitForUpdate()
328
+
329
+ expect(inputTag.name).to.equal('observed-change')
330
+ })
331
+
332
+ it('should detect child node mutations via MutationObserver', async () => {
333
+ document.body.innerHTML = `
334
+ <input-tag name="child-observer" multiple>
335
+ <tag-option value="original">Original</tag-option>
336
+ </input-tag>
337
+ `
338
+ const inputTag = document.querySelector('input-tag')
339
+ await waitForElement(inputTag, '_taggle')
340
+
341
+ expect(getTagValues(inputTag)).to.deep.equal(['original'])
342
+
343
+ // Add child that should be observed
344
+ const newChild = document.createElement('tag-option')
345
+ newChild.setAttribute('value', 'observed')
346
+ newChild.textContent = 'Observed'
347
+ inputTag.appendChild(newChild)
348
+ await waitForUpdate()
349
+
350
+ expect(getTagValues(inputTag)).to.deep.equal(['original', 'observed'])
351
+ })
352
+
353
+ it('should clean up MutationObserver on disconnect', async () => {
354
+ document.body.innerHTML = `
355
+ <input-tag name="cleanup-test" multiple>
356
+ <tag-option value="test">Test</tag-option>
357
+ </input-tag>
358
+ `
359
+ const inputTag = document.querySelector('input-tag')
360
+ await waitForElement(inputTag, '_taggle')
361
+
362
+ expect(inputTag.observer).to.not.be.null
363
+
364
+ // Disconnect should clean up observer
365
+ inputTag.disconnectedCallback()
366
+
367
+ // Observer should be cleaned up (can't directly test this, but ensure no errors)
368
+ expect(() => {
369
+ inputTag.setAttribute('name', 'should-not-crash')
370
+ }).to.not.throw()
371
+ })
372
+ })
373
+
374
+ describe('Edge Cases', () => {
375
+ it('should handle malformed tag-option mutations gracefully', async () => {
376
+ document.body.innerHTML = `
377
+ <input-tag name="malformed" multiple></input-tag>
378
+ `
379
+ const inputTag = document.querySelector('input-tag')
380
+ await waitForElement(inputTag, '_taggle')
381
+
382
+ // Add malformed tag-option (no value attribute)
383
+ const malformedOption = document.createElement('tag-option')
384
+ malformedOption.textContent = 'No Value'
385
+ inputTag.appendChild(malformedOption)
386
+ await waitForUpdate()
387
+
388
+ // Should handle gracefully without crashing
389
+ expect(() => getTagValues(inputTag)).to.not.throw()
390
+ })
391
+
392
+ it('should handle rapid mutations without race conditions', async () => {
393
+ document.body.innerHTML = `
394
+ <input-tag name="rapid" multiple></input-tag>
395
+ `
396
+ const inputTag = document.querySelector('input-tag')
397
+ await waitForElement(inputTag, '_taggle')
398
+
399
+ // Rapid mutations
400
+ for (let i = 0; i < 10; i++) {
401
+ const option = document.createElement('tag-option')
402
+ option.setAttribute('value', `rapid-${i}`)
403
+ option.textContent = `Rapid ${i}`
404
+ inputTag.appendChild(option)
405
+ }
406
+
407
+ await waitForUpdate(100) // Give extra time for processing
408
+
409
+ expect(getTagElements(inputTag)).to.have.length(10)
410
+ expect(getTagValues(inputTag)).to.include('rapid-0')
411
+ expect(getTagValues(inputTag)).to.include('rapid-9')
412
+ })
413
+ })
414
+
415
+ describe('Form Integration with Mutations', () => {
416
+ it('should update FormData when name attribute changes', async () => {
417
+ document.body.innerHTML = `
418
+ <form>
419
+ <input-tag name="original-form-name" multiple>
420
+ <tag-option value="form-test">Form Test</tag-option>
421
+ </input-tag>
422
+ </form>
423
+ `
424
+ const form = document.querySelector('form')
425
+ const inputTag = document.querySelector('input-tag')
426
+ await waitForElement(inputTag, '_taggle')
427
+
428
+ let formData = new FormData(form)
429
+ expect(formData.getAll('original-form-name')).to.deep.equal(['form-test'])
430
+
431
+ // Change name via mutation
432
+ inputTag.setAttribute('name', 'mutated-form-name')
433
+ await waitForUpdate()
434
+
435
+ formData = new FormData(form)
436
+ expect(formData.getAll('mutated-form-name')).to.deep.equal(['form-test'])
437
+ expect(formData.getAll('original-form-name')).to.deep.equal([])
438
+ })
439
+
440
+ it('should maintain form validity state through mutations', async () => {
441
+ document.body.innerHTML = `
442
+ <form>
443
+ <input-tag name="validity-test" multiple>
444
+ <tag-option value="valid">Valid</tag-option>
445
+ </input-tag>
446
+ </form>
447
+ `
448
+ const inputTag = document.querySelector('input-tag')
449
+ await waitForElement(inputTag, '_taggle')
450
+
451
+ expect(inputTag.checkValidity()).to.be.true
452
+
453
+ // Add required attribute
454
+ inputTag.setAttribute('required', '')
455
+ await waitForUpdate()
456
+
457
+ expect(inputTag.checkValidity()).to.be.true // Still valid due to existing tag
458
+
459
+ // Remove all tags
460
+ inputTag.innerHTML = ''
461
+ await waitForUpdate()
462
+
463
+ expect(inputTag.checkValidity()).to.be.false // Now invalid
464
+ })
465
+ })
466
+ })