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,425 @@
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
+ expectEventToFire,
9
+ simulateFocus,
10
+ simulateBlur,
11
+ getTagElements,
12
+ getTagValues,
13
+ clickTagRemoveButton
14
+ } from './lib/test-utils.js'
15
+
16
+ describe('Events', () => {
17
+ setupGlobalTestHooks()
18
+
19
+ describe('Change Events', () => {
20
+ it('should fire change event when tags are added', async () => {
21
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
22
+
23
+ const eventPromise = expectEventToFire(inputTag, 'change')
24
+
25
+ inputTag.add('new-tag')
26
+ await waitForUpdate()
27
+
28
+ const { eventFired, eventData } = await eventPromise
29
+ expect(eventFired).to.be.true
30
+ expect(eventData.bubbles).to.be.true
31
+ expect(eventData.composed).to.be.true
32
+ })
33
+
34
+ it('should fire change event when tags are removed', async () => {
35
+ const inputTag = await setupInputTag(`
36
+ <input-tag name="tags" multiple>
37
+ <tag-option value="remove-me">Remove Me</tag-option>
38
+ </input-tag>
39
+ `)
40
+
41
+ const eventPromise = expectEventToFire(inputTag, 'change')
42
+
43
+ inputTag.remove('remove-me')
44
+ await waitForUpdate()
45
+
46
+ const { eventFired } = await eventPromise
47
+ expect(eventFired).to.be.true
48
+ })
49
+
50
+ it('should fire change event when array value is set programmatically in multiple mode', async () => {
51
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
52
+
53
+ const eventPromise = expectEventToFire(inputTag, 'change')
54
+
55
+ inputTag.value = ['programmatic-tag']
56
+ await waitForUpdate()
57
+
58
+ const { eventFired } = await eventPromise
59
+ expect(eventFired).to.be.true
60
+ })
61
+
62
+ it('should fire change event when string value is set programmatically in single mode', async () => {
63
+ const inputTag = await setupInputTag('<input-tag name="tag"></input-tag>')
64
+
65
+ const eventPromise = expectEventToFire(inputTag, 'change')
66
+
67
+ inputTag.value = 'programmatic-tag'
68
+ await waitForUpdate()
69
+
70
+ const { eventFired } = await eventPromise
71
+ expect(eventFired).to.be.true
72
+ })
73
+
74
+ it('should not fire change event during initialization', async () => {
75
+ let changeEventFired = false
76
+
77
+ document.body.innerHTML = `
78
+ <input-tag name="tags" multiple>
79
+ <tag-option value="initial">Initial</tag-option>
80
+ </input-tag>
81
+ `
82
+ const inputTag = document.querySelector('input-tag')
83
+
84
+ inputTag.addEventListener('change', () => {
85
+ changeEventFired = true
86
+ })
87
+
88
+ await waitForUpdate(200) // Wait longer for initialization
89
+
90
+ expect(changeEventFired).to.be.false
91
+ })
92
+
93
+ it('should not fire change event when suppressEvents is true', async () => {
94
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
95
+
96
+ let changeEventFired = false
97
+ inputTag.addEventListener('change', () => {
98
+ changeEventFired = true
99
+ })
100
+
101
+ inputTag.suppressEvents = true
102
+ inputTag.add('suppressed-tag')
103
+ await waitForUpdate()
104
+
105
+ expect(changeEventFired).to.be.false
106
+ })
107
+
108
+ it('should not fire change event when array values do not actually change in multiple mode', async () => {
109
+ const inputTag = await setupInputTag(`
110
+ <input-tag name="tags" multiple>
111
+ <tag-option value="existing">Existing</tag-option>
112
+ </input-tag>
113
+ `)
114
+
115
+ let changeEventFired = false
116
+ inputTag.addEventListener('change', () => {
117
+ changeEventFired = true
118
+ })
119
+
120
+ // Set the same value again
121
+ inputTag.value = ['existing']
122
+ await waitForUpdate()
123
+
124
+ expect(changeEventFired).to.be.false
125
+ })
126
+
127
+ it('should not fire change event when string value does not actually change in single mode', async () => {
128
+ const inputTag = await setupInputTag(`
129
+ <input-tag name="tag">
130
+ <tag-option value="existing">Existing</tag-option>
131
+ </input-tag>
132
+ `)
133
+
134
+ let changeEventFired = false
135
+ inputTag.addEventListener('change', () => {
136
+ changeEventFired = true
137
+ })
138
+
139
+ // Set the same value again
140
+ inputTag.value = 'existing'
141
+ await waitForUpdate()
142
+
143
+ expect(changeEventFired).to.be.false
144
+ })
145
+ })
146
+
147
+ describe('Update Events', () => {
148
+ it('should fire update event when tag is added with correct detail', async () => {
149
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
150
+
151
+ const eventPromise = expectEventToFire(inputTag, 'update')
152
+
153
+ inputTag.add('test-tag')
154
+ await waitForUpdate()
155
+
156
+ const { eventFired, eventData } = await eventPromise
157
+ expect(eventFired).to.be.true
158
+ expect(eventData.detail.tag).to.equal('test-tag')
159
+ expect(eventData.bubbles).to.be.true
160
+ expect(eventData.composed).to.be.true
161
+ })
162
+
163
+ it('should fire update event when tag is removed with correct detail', async () => {
164
+ const inputTag = await setupInputTag(`
165
+ <input-tag name="tags" multiple>
166
+ <tag-option value="remove-me">Remove Me</tag-option>
167
+ </input-tag>
168
+ `)
169
+
170
+ const eventPromise = expectEventToFire(inputTag, 'update')
171
+
172
+ inputTag.remove('remove-me')
173
+ await waitForUpdate()
174
+
175
+ const { eventFired, eventData } = await eventPromise
176
+ expect(eventFired).to.be.true
177
+ expect(eventData.detail.tag).to.equal('remove-me')
178
+ })
179
+
180
+ it('should indicate if tag is new in update event detail', async () => {
181
+ const inputTag = await setupInputTag(`
182
+ <input-tag name="frameworks" list="suggestions" multiple></input-tag>
183
+ <datalist id="suggestions">
184
+ <option value="react">React</option>
185
+ <option value="vue">Vue</option>
186
+ </datalist>
187
+ `)
188
+
189
+ // Add a tag that exists in options (not new)
190
+ let eventPromise = expectEventToFire(inputTag, 'update')
191
+ inputTag.add('react')
192
+ await waitForUpdate()
193
+
194
+ let { eventData } = await eventPromise
195
+ expect(eventData.detail.isNew).to.be.false
196
+
197
+ // Add a tag that doesn't exist in options (is new)
198
+ eventPromise = expectEventToFire(inputTag, 'update')
199
+ inputTag.add('custom-framework')
200
+ await waitForUpdate()
201
+
202
+ const result = await eventPromise
203
+ expect(result.eventData.detail.isNew).to.be.true
204
+ })
205
+
206
+ it('should not fire update event when suppressEvents is true', async () => {
207
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
208
+
209
+ let updateEventFired = false
210
+ inputTag.addEventListener('update', () => {
211
+ updateEventFired = true
212
+ })
213
+
214
+ inputTag.suppressEvents = true
215
+ inputTag.add('suppressed-tag')
216
+ await waitForUpdate()
217
+
218
+ expect(updateEventFired).to.be.false
219
+ })
220
+
221
+ it('should fire separate update events for multiple tag operations', async () => {
222
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
223
+
224
+ const updateEvents = []
225
+ inputTag.addEventListener('update', (e) => {
226
+ updateEvents.push(e.detail.tag)
227
+ })
228
+
229
+ inputTag.add('first')
230
+ await waitForUpdate()
231
+ inputTag.add('second')
232
+ await waitForUpdate()
233
+
234
+ expect(updateEvents).to.deep.equal(['first', 'second'])
235
+ })
236
+ })
237
+
238
+ describe('Focus and Blur Events', () => {
239
+ it('should handle focus on input-tag element', async () => {
240
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
241
+
242
+ const eventPromise = expectEventToFire(inputTag, 'focus')
243
+
244
+ simulateFocus(inputTag)
245
+
246
+ const { eventFired } = await eventPromise
247
+ expect(eventFired).to.be.true
248
+ })
249
+
250
+ it('should focus internal input when input-tag is focused', async () => {
251
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
252
+
253
+ const focusSpy = spy(inputTag._taggleInputTarget, 'focus')
254
+
255
+ inputTag.focus()
256
+ await waitForUpdate()
257
+
258
+ expect(focusSpy.called).to.be.true
259
+ focusSpy.restore()
260
+ })
261
+
262
+ it('should handle blur events properly', async () => {
263
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
264
+
265
+ inputTag.focus()
266
+ await waitForUpdate()
267
+
268
+ const eventPromise = expectEventToFire(inputTag._taggleInputTarget, 'blur')
269
+
270
+ simulateBlur(inputTag._taggleInputTarget)
271
+
272
+ const { eventFired } = await eventPromise
273
+ expect(eventFired).to.be.true
274
+ })
275
+ })
276
+
277
+ describe('Event Bubbling and Composition', () => {
278
+ it('should have change events that bubble', async () => {
279
+ document.body.innerHTML = `
280
+ <div id="parent">
281
+ <input-tag name="tags" multiple></input-tag>
282
+ </div>
283
+ `
284
+ const parent = document.querySelector('#parent')
285
+ const inputTag = document.querySelector('input-tag')
286
+
287
+ await waitForUpdate(100) // Wait for initialization
288
+
289
+ const eventPromise = expectEventToFire(parent, 'change')
290
+
291
+ inputTag.add('bubbling-tag')
292
+ await waitForUpdate()
293
+
294
+ const { eventFired } = await eventPromise
295
+ expect(eventFired).to.be.true
296
+ })
297
+
298
+ it('should have update events that bubble', async () => {
299
+ document.body.innerHTML = `
300
+ <div id="parent">
301
+ <input-tag name="tags" multiple></input-tag>
302
+ </div>
303
+ `
304
+ const parent = document.querySelector('#parent')
305
+ const inputTag = document.querySelector('input-tag')
306
+
307
+ await waitForUpdate(100) // Wait for initialization
308
+
309
+ const eventPromise = expectEventToFire(parent, 'update')
310
+
311
+ inputTag.add('bubbling-update')
312
+ await waitForUpdate()
313
+
314
+ const { eventFired } = await eventPromise
315
+ expect(eventFired).to.be.true
316
+ })
317
+
318
+ it('should have composed events that cross shadow DOM boundaries', async () => {
319
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
320
+
321
+ let composedEventReceived = false
322
+ document.addEventListener('change', (e) => {
323
+ if (e.target === inputTag) {
324
+ composedEventReceived = true
325
+ }
326
+ })
327
+
328
+ inputTag.add('composed-tag')
329
+ await waitForUpdate()
330
+
331
+ expect(composedEventReceived).to.be.true
332
+
333
+ // Clean up
334
+ document.removeEventListener('change', () => {})
335
+ })
336
+ })
337
+
338
+ describe('Event Timing and Sequence', () => {
339
+ it('should fire update event before change event', async () => {
340
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
341
+
342
+ const eventSequence = []
343
+
344
+ inputTag.addEventListener('update', () => {
345
+ eventSequence.push('update')
346
+ })
347
+
348
+ inputTag.addEventListener('change', () => {
349
+ eventSequence.push('change')
350
+ })
351
+
352
+ inputTag.add('sequence-test')
353
+ await waitForUpdate()
354
+
355
+ expect(eventSequence).to.deep.equal(['update', 'change'])
356
+ })
357
+
358
+ it('should handle rapid event firing without issues', async () => {
359
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
360
+
361
+ const updateEvents = []
362
+ const changeEvents = []
363
+
364
+ inputTag.addEventListener('update', (e) => {
365
+ updateEvents.push(e.detail.tag)
366
+ })
367
+
368
+ inputTag.addEventListener('change', () => {
369
+ changeEvents.push('change')
370
+ })
371
+
372
+ // Rapid additions
373
+ inputTag.add('rapid1')
374
+ inputTag.add('rapid2')
375
+ inputTag.add('rapid3')
376
+ await waitForUpdate(100)
377
+
378
+ expect(updateEvents).to.have.length(3)
379
+ expect(changeEvents).to.have.length(3)
380
+ })
381
+ })
382
+
383
+ describe('Event Cleanup and Memory Management', () => {
384
+ it('should remove event listeners on disconnect', async () => {
385
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
386
+
387
+ const form = document.createElement('form')
388
+ form.appendChild(inputTag)
389
+
390
+ // Simulate disconnect
391
+ inputTag.disconnectedCallback()
392
+
393
+ // Should not throw or cause memory leaks
394
+ expect(() => {
395
+ form.reset()
396
+ }).to.not.throw()
397
+ })
398
+
399
+ it('should handle form reset events properly after disconnect and reconnect', async () => {
400
+ document.body.innerHTML = `
401
+ <form>
402
+ <input-tag name="tags" multiple>
403
+ <tag-option value="test">Test</tag-option>
404
+ </input-tag>
405
+ </form>
406
+ `
407
+ const form = document.querySelector('form')
408
+ const inputTag = document.querySelector('input-tag')
409
+
410
+ await waitForUpdate(100)
411
+
412
+ // Remove and re-add to DOM
413
+ form.removeChild(inputTag)
414
+ await waitForUpdate()
415
+ form.appendChild(inputTag)
416
+ await waitForUpdate()
417
+
418
+ // Should still work
419
+ form.reset()
420
+ await waitForUpdate()
421
+
422
+ expect(getTagElements(inputTag)).to.have.length(0)
423
+ })
424
+ })
425
+ })