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,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
|
+
})
|