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.
@@ -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
+ })