bard-tag_field 0.5.1 → 0.5.3
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 -1
- data/app/assets/javascripts/input-tag.js +16 -30
- 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/version.rb +1 -1
- metadata +23 -4
- data/input-tag/bun.lockb +0 -0
- data/input-tag/index.js +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { expect } from '@esm-bundle/chai'
|
|
2
|
+
|
|
3
|
+
export function setupGlobalTestHooks() {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
document.body.innerHTML = ''
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
document.body.innerHTML = ''
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function waitForElement(element, property, timeout = 1000) {
|
|
14
|
+
const start = Date.now()
|
|
15
|
+
while (!element[property] && (Date.now() - start) < timeout) {
|
|
16
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
17
|
+
}
|
|
18
|
+
return element[property]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function setupInputTag(html) {
|
|
22
|
+
document.body.innerHTML = html
|
|
23
|
+
const inputTag = document.querySelector('input-tag')
|
|
24
|
+
|
|
25
|
+
await waitForElement(inputTag, '_taggle')
|
|
26
|
+
|
|
27
|
+
return inputTag
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Wait for basic initialization without requiring internal access
|
|
31
|
+
export async function waitForBasicInitialization(inputTag, timeout = 1000) {
|
|
32
|
+
const start = Date.now()
|
|
33
|
+
while ((Date.now() - start) < timeout) {
|
|
34
|
+
// Wait for the component to be ready by checking if basic properties work
|
|
35
|
+
if (inputTag.tags !== undefined && typeof inputTag.add === 'function' && inputTag._taggle) {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
39
|
+
}
|
|
40
|
+
throw new Error('InputTag failed basic initialization within timeout')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function waitForUpdate(ms = 50) {
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, ms))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function simulateKeydown(element, keyCode, options = {}) {
|
|
48
|
+
const event = new KeyboardEvent('keydown', {
|
|
49
|
+
keyCode,
|
|
50
|
+
which: keyCode,
|
|
51
|
+
bubbles: true,
|
|
52
|
+
...options
|
|
53
|
+
})
|
|
54
|
+
element.dispatchEvent(event)
|
|
55
|
+
return event
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function simulateKeyup(element, keyCode, options = {}) {
|
|
59
|
+
const event = new KeyboardEvent('keyup', {
|
|
60
|
+
keyCode,
|
|
61
|
+
which: keyCode,
|
|
62
|
+
bubbles: true,
|
|
63
|
+
...options
|
|
64
|
+
})
|
|
65
|
+
element.dispatchEvent(event)
|
|
66
|
+
return event
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function simulateInput(element, value) {
|
|
70
|
+
element.value = value
|
|
71
|
+
const event = new Event('input', { bubbles: true })
|
|
72
|
+
element.dispatchEvent(event)
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, 20))
|
|
74
|
+
return event
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Simulate user typing in the input-tag's internal input
|
|
78
|
+
export function simulateUserInput(inputTag, value) {
|
|
79
|
+
// Focus the element first (user would click/focus)
|
|
80
|
+
inputTag.focus()
|
|
81
|
+
|
|
82
|
+
// Find the actual input element by looking for it in the shadow DOM
|
|
83
|
+
const input = findInternalInput(inputTag)
|
|
84
|
+
if (input) {
|
|
85
|
+
simulateInput(input, value)
|
|
86
|
+
}
|
|
87
|
+
return input
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Helper to find the internal input without using private properties
|
|
91
|
+
function findInternalInput(inputTag) {
|
|
92
|
+
// Look for input elements in the shadow DOM
|
|
93
|
+
const inputs = inputTag.shadowRoot?.querySelectorAll('input')
|
|
94
|
+
// Return the first input that's not hidden
|
|
95
|
+
return Array.from(inputs || []).find(input =>
|
|
96
|
+
input.type !== 'hidden' &&
|
|
97
|
+
getComputedStyle(input).display !== 'none'
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Simulate user typing and pressing enter to add a tag
|
|
102
|
+
export async function simulateUserAddTag(inputTag, value) {
|
|
103
|
+
const input = simulateUserInput(inputTag, value)
|
|
104
|
+
if (input) {
|
|
105
|
+
simulateKeydown(input, KEYCODES.ENTER)
|
|
106
|
+
}
|
|
107
|
+
await waitForUpdate()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Simulate user typing and pressing specific key to add a tag
|
|
111
|
+
export async function simulateUserAddTagWithKey(inputTag, value, keyCode) {
|
|
112
|
+
const input = simulateUserInput(inputTag, value)
|
|
113
|
+
if (input) {
|
|
114
|
+
simulateKeydown(input, keyCode)
|
|
115
|
+
}
|
|
116
|
+
await waitForUpdate()
|
|
117
|
+
return input
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function simulateClick(element) {
|
|
121
|
+
const event = new MouseEvent('click', { bubbles: true })
|
|
122
|
+
element.dispatchEvent(event)
|
|
123
|
+
return event
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function simulateFocus(element) {
|
|
127
|
+
const event = new FocusEvent('focus', { bubbles: true })
|
|
128
|
+
element.dispatchEvent(event)
|
|
129
|
+
return event
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function simulateBlur(element) {
|
|
133
|
+
const event = new FocusEvent('blur', { bubbles: true })
|
|
134
|
+
element.dispatchEvent(event)
|
|
135
|
+
return event
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function expectEventToFire(element, eventName) {
|
|
139
|
+
return new Promise(resolve => {
|
|
140
|
+
let eventFired = false
|
|
141
|
+
let eventData = null
|
|
142
|
+
|
|
143
|
+
const handler = (e) => {
|
|
144
|
+
eventFired = true
|
|
145
|
+
eventData = {
|
|
146
|
+
type: e.type,
|
|
147
|
+
target: e.target,
|
|
148
|
+
detail: e.detail,
|
|
149
|
+
bubbles: e.bubbles,
|
|
150
|
+
composed: e.composed
|
|
151
|
+
}
|
|
152
|
+
element.removeEventListener(eventName, handler)
|
|
153
|
+
resolve({ eventFired, eventData })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
element.addEventListener(eventName, handler)
|
|
157
|
+
|
|
158
|
+
// Resolve after a timeout if event doesn't fire
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
element.removeEventListener(eventName, handler)
|
|
161
|
+
resolve({ eventFired, eventData })
|
|
162
|
+
}, 100)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function getTagElements(inputTag) {
|
|
167
|
+
return Array.from(inputTag.children).filter(el => el.tagName.toLowerCase() === 'tag-option')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function getTagValues(inputTag) {
|
|
171
|
+
return getTagElements(inputTag).map(el => el.value)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function clickTagRemoveButton(tagElement) {
|
|
175
|
+
const button = tagElement.shadowRoot.querySelector('button')
|
|
176
|
+
simulateClick(button)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const KEYCODES = {
|
|
180
|
+
BACKSPACE: 8,
|
|
181
|
+
TAB: 9,
|
|
182
|
+
ENTER: 13,
|
|
183
|
+
DELETE: 46,
|
|
184
|
+
COMMA: 188,
|
|
185
|
+
ARROW_LEFT: 37,
|
|
186
|
+
ARROW_RIGHT: 39
|
|
187
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { expect } from '@esm-bundle/chai'
|
|
2
|
+
import '../src/input-tag.js'
|
|
3
|
+
import {
|
|
4
|
+
setupGlobalTestHooks,
|
|
5
|
+
setupInputTag,
|
|
6
|
+
waitForUpdate,
|
|
7
|
+
waitForBasicInitialization,
|
|
8
|
+
simulateInput,
|
|
9
|
+
getTagElements,
|
|
10
|
+
getTagValues,
|
|
11
|
+
} from './lib/test-utils.js'
|
|
12
|
+
|
|
13
|
+
describe('Nested Datalist Support', () => {
|
|
14
|
+
setupGlobalTestHooks()
|
|
15
|
+
|
|
16
|
+
describe('Bug Reproduction', () => {
|
|
17
|
+
it('should not include null values from datalist elements in tags array', async () => {
|
|
18
|
+
const inputTag = await setupInputTag(`
|
|
19
|
+
<input-tag name="test" multiple>
|
|
20
|
+
<datalist>
|
|
21
|
+
<option value="js">JavaScript</option>
|
|
22
|
+
<option value="py">Python</option>
|
|
23
|
+
</datalist>
|
|
24
|
+
</input-tag>
|
|
25
|
+
`)
|
|
26
|
+
|
|
27
|
+
// Should have no tags initially (datalist should not be converted to tags)
|
|
28
|
+
expect(inputTag.tags).to.deep.equal([])
|
|
29
|
+
expect(inputTag.tags).to.not.include(null)
|
|
30
|
+
expect(inputTag.tags).to.not.include(undefined)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should not add null when selecting from nested datalist autocomplete', async () => {
|
|
34
|
+
const inputTag = await setupInputTag(`
|
|
35
|
+
<input-tag name="frameworks" multiple>
|
|
36
|
+
<datalist>
|
|
37
|
+
<option value="nextjs">Next.js Framework</option>
|
|
38
|
+
</datalist>
|
|
39
|
+
</input-tag>
|
|
40
|
+
`)
|
|
41
|
+
|
|
42
|
+
// Initially should be empty
|
|
43
|
+
expect(inputTag.tags).to.deep.equal([])
|
|
44
|
+
|
|
45
|
+
const input = inputTag._taggleInputTarget
|
|
46
|
+
await simulateInput(input, 'next')
|
|
47
|
+
|
|
48
|
+
// Click the autocomplete suggestion
|
|
49
|
+
const autocompleteItems = inputTag.autocompleteContainerTarget.querySelectorAll('.ui-menu-item')
|
|
50
|
+
expect(autocompleteItems.length).to.equal(1)
|
|
51
|
+
autocompleteItems[0].click()
|
|
52
|
+
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
54
|
+
|
|
55
|
+
// Should only have the selected value, no nulls
|
|
56
|
+
expect(inputTag.tags).to.deep.equal(['nextjs'])
|
|
57
|
+
expect(inputTag.tags).to.not.include(null)
|
|
58
|
+
expect(inputTag.tags).to.not.include(undefined)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('Basic Nested Datalist Functionality', () => {
|
|
63
|
+
it('should read options from nested datalist element', async () => {
|
|
64
|
+
const inputTag = await setupInputTag(`
|
|
65
|
+
<input-tag name="languages" multiple>
|
|
66
|
+
<datalist>
|
|
67
|
+
<option value="js">JavaScript</option>
|
|
68
|
+
<option value="py">Python</option>
|
|
69
|
+
<option value="rb">Ruby</option>
|
|
70
|
+
</datalist>
|
|
71
|
+
</input-tag>
|
|
72
|
+
`)
|
|
73
|
+
|
|
74
|
+
// Options should be available from nested datalist
|
|
75
|
+
expect(inputTag.options).to.deep.equal(['js', 'py', 'rb'])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should support value/label separation with nested datalist', async () => {
|
|
79
|
+
const inputTag = await setupInputTag(`
|
|
80
|
+
<input-tag name="frameworks" multiple>
|
|
81
|
+
<datalist>
|
|
82
|
+
<option value="react">React - Component Library</option>
|
|
83
|
+
<option value="vue">Vue.js - Progressive Framework</option>
|
|
84
|
+
<option value="angular">Angular - Full Framework</option>
|
|
85
|
+
</datalist>
|
|
86
|
+
</input-tag>
|
|
87
|
+
`)
|
|
88
|
+
|
|
89
|
+
const optionsWithLabels = inputTag._getOptionsWithLabels()
|
|
90
|
+
expect(optionsWithLabels).to.deep.equal([
|
|
91
|
+
{ value: 'react', label: 'React - Component Library' },
|
|
92
|
+
{ value: 'vue', label: 'Vue.js - Progressive Framework' },
|
|
93
|
+
{ value: 'angular', label: 'Angular - Full Framework' }
|
|
94
|
+
])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should prioritize external datalist over nested datalist', async () => {
|
|
98
|
+
document.body.innerHTML = `
|
|
99
|
+
<input-tag name="test" list="external-datalist" multiple>
|
|
100
|
+
<datalist>
|
|
101
|
+
<option value="nested">Nested Option</option>
|
|
102
|
+
</datalist>
|
|
103
|
+
</input-tag>
|
|
104
|
+
<datalist id="external-datalist">
|
|
105
|
+
<option value="external">External Option</option>
|
|
106
|
+
</datalist>
|
|
107
|
+
`
|
|
108
|
+
|
|
109
|
+
const inputTag = document.querySelector('input-tag')
|
|
110
|
+
await waitForBasicInitialization(inputTag)
|
|
111
|
+
|
|
112
|
+
// Should use external datalist, not nested one
|
|
113
|
+
expect(inputTag.options).to.deep.equal(['external'])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should fall back to nested datalist when external datalist not found', async () => {
|
|
117
|
+
const inputTag = await setupInputTag(`
|
|
118
|
+
<input-tag name="test" list="nonexistent-datalist" multiple>
|
|
119
|
+
<datalist>
|
|
120
|
+
<option value="nested">Nested Option</option>
|
|
121
|
+
</datalist>
|
|
122
|
+
</input-tag>
|
|
123
|
+
`)
|
|
124
|
+
|
|
125
|
+
// Should fall back to nested datalist
|
|
126
|
+
expect(inputTag.options).to.deep.equal(['nested'])
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('Nested Datalist with Autocomplete', () => {
|
|
131
|
+
it('should work with autocomplete using nested datalist', async () => {
|
|
132
|
+
const inputTag = await setupInputTag(`
|
|
133
|
+
<input-tag name="languages" multiple>
|
|
134
|
+
<datalist>
|
|
135
|
+
<option value="js">JavaScript Framework</option>
|
|
136
|
+
<option value="py">Python Language</option>
|
|
137
|
+
<option value="rb">Ruby Language</option>
|
|
138
|
+
</datalist>
|
|
139
|
+
</input-tag>
|
|
140
|
+
`)
|
|
141
|
+
|
|
142
|
+
const input = inputTag._taggleInputTarget
|
|
143
|
+
|
|
144
|
+
// Simulate typing to trigger autocomplete
|
|
145
|
+
await simulateInput(input, 'java')
|
|
146
|
+
|
|
147
|
+
// Should find JavaScript by label
|
|
148
|
+
expect(inputTag._autocompleteSuggestions).to.deep.equal(['JavaScript Framework'])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should create correct tag-option when selecting from nested datalist autocomplete', async () => {
|
|
152
|
+
const inputTag = await setupInputTag(`
|
|
153
|
+
<input-tag name="frameworks" multiple>
|
|
154
|
+
<datalist>
|
|
155
|
+
<option value="vue">Vue.js Framework</option>
|
|
156
|
+
<option value="react">React Library</option>
|
|
157
|
+
</datalist>
|
|
158
|
+
</input-tag>
|
|
159
|
+
`)
|
|
160
|
+
|
|
161
|
+
const input = inputTag._taggleInputTarget
|
|
162
|
+
await simulateInput(input, 'vue')
|
|
163
|
+
|
|
164
|
+
// Click the autocomplete suggestion
|
|
165
|
+
const autocompleteItems = inputTag.autocompleteContainerTarget.querySelectorAll('.ui-menu-item')
|
|
166
|
+
expect(autocompleteItems.length).to.equal(1)
|
|
167
|
+
autocompleteItems[0].click()
|
|
168
|
+
|
|
169
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
170
|
+
|
|
171
|
+
const tagElements = getTagElements(inputTag)
|
|
172
|
+
expect(tagElements.length).to.equal(1)
|
|
173
|
+
expect(tagElements[0].value).to.equal('vue')
|
|
174
|
+
expect(tagElements[0].textContent.trim()).to.equal('Vue.js Framework')
|
|
175
|
+
expect(inputTag.tags.filter(tag => tag !== undefined)).to.deep.equal(['vue'])
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('Dynamic Nested Datalist Changes', () => {
|
|
180
|
+
it('should update autocomplete when nested datalist is added dynamically', async () => {
|
|
181
|
+
const inputTag = await setupInputTag(`
|
|
182
|
+
<input-tag name="test" multiple></input-tag>
|
|
183
|
+
`)
|
|
184
|
+
|
|
185
|
+
// Initially no options
|
|
186
|
+
expect(inputTag.options).to.deep.equal([])
|
|
187
|
+
|
|
188
|
+
// Add nested datalist dynamically
|
|
189
|
+
const datalist = document.createElement('datalist')
|
|
190
|
+
datalist.innerHTML = `
|
|
191
|
+
<option value="new">New Option</option>
|
|
192
|
+
`
|
|
193
|
+
inputTag.appendChild(datalist)
|
|
194
|
+
|
|
195
|
+
// Wait for mutation observer
|
|
196
|
+
await waitForUpdate()
|
|
197
|
+
|
|
198
|
+
// Should now have options
|
|
199
|
+
expect(inputTag.options).to.deep.equal(['new'])
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('should update autocomplete when nested datalist options are modified', async () => {
|
|
203
|
+
const inputTag = await setupInputTag(`
|
|
204
|
+
<input-tag name="test" multiple>
|
|
205
|
+
<datalist id="nested-list">
|
|
206
|
+
<option value="original">Original Option</option>
|
|
207
|
+
</datalist>
|
|
208
|
+
</input-tag>
|
|
209
|
+
`)
|
|
210
|
+
|
|
211
|
+
expect(inputTag.options).to.deep.equal(['original'])
|
|
212
|
+
|
|
213
|
+
// Add new option to existing nested datalist
|
|
214
|
+
const datalist = inputTag.querySelector('datalist')
|
|
215
|
+
const newOption = document.createElement('option')
|
|
216
|
+
newOption.value = 'added'
|
|
217
|
+
newOption.textContent = 'Added Option'
|
|
218
|
+
datalist.appendChild(newOption)
|
|
219
|
+
|
|
220
|
+
// Wait for mutation observer
|
|
221
|
+
await waitForUpdate()
|
|
222
|
+
|
|
223
|
+
// Should include new option
|
|
224
|
+
expect(inputTag.options).to.deep.equal(['original', 'added'])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('should remove autocomplete when nested datalist is removed', async () => {
|
|
228
|
+
const inputTag = await setupInputTag(`
|
|
229
|
+
<input-tag name="test" multiple>
|
|
230
|
+
<datalist>
|
|
231
|
+
<option value="temp">Temporary Option</option>
|
|
232
|
+
</datalist>
|
|
233
|
+
</input-tag>
|
|
234
|
+
`)
|
|
235
|
+
|
|
236
|
+
expect(inputTag.options).to.deep.equal(['temp'])
|
|
237
|
+
|
|
238
|
+
// Remove nested datalist
|
|
239
|
+
const datalist = inputTag.querySelector('datalist')
|
|
240
|
+
datalist.remove()
|
|
241
|
+
|
|
242
|
+
// Wait for mutation observer
|
|
243
|
+
await waitForUpdate()
|
|
244
|
+
|
|
245
|
+
// Should have no options
|
|
246
|
+
expect(inputTag.options).to.deep.equal([])
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe('Nested Datalist Edge Cases', () => {
|
|
251
|
+
it('should handle multiple nested datalists (use first one)', async () => {
|
|
252
|
+
const inputTag = await setupInputTag(`
|
|
253
|
+
<input-tag name="test" multiple>
|
|
254
|
+
<datalist>
|
|
255
|
+
<option value="first">First Datalist</option>
|
|
256
|
+
</datalist>
|
|
257
|
+
<datalist>
|
|
258
|
+
<option value="second">Second Datalist</option>
|
|
259
|
+
</datalist>
|
|
260
|
+
</input-tag>
|
|
261
|
+
`)
|
|
262
|
+
|
|
263
|
+
// Should use the first datalist found
|
|
264
|
+
expect(inputTag.options).to.deep.equal(['first'])
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should handle empty nested datalist', async () => {
|
|
268
|
+
const inputTag = await setupInputTag(`
|
|
269
|
+
<input-tag name="test" multiple>
|
|
270
|
+
<datalist></datalist>
|
|
271
|
+
</input-tag>
|
|
272
|
+
`)
|
|
273
|
+
|
|
274
|
+
expect(inputTag.options).to.deep.equal([])
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should handle nested datalist with options without values', async () => {
|
|
278
|
+
const inputTag = await setupInputTag(`
|
|
279
|
+
<input-tag name="test" multiple>
|
|
280
|
+
<datalist>
|
|
281
|
+
<option>Option Without Value</option>
|
|
282
|
+
<option value="">Empty Value</option>
|
|
283
|
+
<option value="valid">Valid Option</option>
|
|
284
|
+
</datalist>
|
|
285
|
+
</input-tag>
|
|
286
|
+
`)
|
|
287
|
+
|
|
288
|
+
// Should include all options - option without value attribute uses textContent as value
|
|
289
|
+
expect(inputTag.options).to.deep.equal(['Option Without Value', '', 'valid'])
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
describe('Backward Compatibility', () => {
|
|
294
|
+
it('should not break existing external datalist functionality', async () => {
|
|
295
|
+
document.body.innerHTML = `
|
|
296
|
+
<input-tag name="test" list="external-list" multiple></input-tag>
|
|
297
|
+
<datalist id="external-list">
|
|
298
|
+
<option value="external">External Option</option>
|
|
299
|
+
</datalist>
|
|
300
|
+
`
|
|
301
|
+
|
|
302
|
+
const inputTag = document.querySelector('input-tag')
|
|
303
|
+
await waitForBasicInitialization(inputTag)
|
|
304
|
+
|
|
305
|
+
// Should still work with external datalist
|
|
306
|
+
expect(inputTag.options).to.deep.equal(['external'])
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('should work correctly with mixed tag-options and nested datalist', async () => {
|
|
310
|
+
const inputTag = await setupInputTag(`
|
|
311
|
+
<input-tag name="mixed" multiple>
|
|
312
|
+
<tag-option value="preset">Preset Tag</tag-option>
|
|
313
|
+
<datalist>
|
|
314
|
+
<option value="option1">Autocomplete Option 1</option>
|
|
315
|
+
<option value="option2">Autocomplete Option 2</option>
|
|
316
|
+
</datalist>
|
|
317
|
+
</input-tag>
|
|
318
|
+
`)
|
|
319
|
+
|
|
320
|
+
// Should have preset tags (filter out any undefined values)
|
|
321
|
+
expect(inputTag.tags.filter(tag => tag !== undefined)).to.deep.equal(['preset'])
|
|
322
|
+
expect(getTagElements(inputTag).filter(el => el.value !== undefined)).to.have.length(1)
|
|
323
|
+
|
|
324
|
+
// And autocomplete options available
|
|
325
|
+
expect(inputTag.options).to.deep.equal(['option1', 'option2'])
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
})
|