enigmatic 0.26.0 → 0.27.0
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.
- package/__tests__/e2.test.js +37 -162
- package/components.js +0 -111
- package/jest.config.js +2 -1
- package/jest.setup.js +5 -0
- package/package.json +3 -2
package/__tests__/e2.test.js
CHANGED
|
@@ -19,199 +19,74 @@ describe('e2.js', () => {
|
|
|
19
19
|
|
|
20
20
|
describe('fetchJson', () => {
|
|
21
21
|
test('fetches JSON with GET method', async () => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
Promise.resolve({
|
|
25
|
-
ok: true,
|
|
26
|
-
status: 200,
|
|
27
|
-
statusText: 'OK',
|
|
28
|
-
headers: new Headers({ 'content-type': 'application/json' }),
|
|
29
|
-
json: () => Promise.resolve(mockData)
|
|
30
|
-
})
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
const result = await window.fetchJson('GET', 'http://test.com/api')
|
|
34
|
-
|
|
35
|
-
expect(global.fetch).toHaveBeenCalledWith(
|
|
36
|
-
'http://test.com/api',
|
|
37
|
-
expect.objectContaining({
|
|
38
|
-
method: 'GET',
|
|
39
|
-
headers: { 'Content-Type': 'application/json' },
|
|
40
|
-
credentials: 'include'
|
|
41
|
-
})
|
|
42
|
-
)
|
|
43
|
-
expect(result.data).toEqual(mockData)
|
|
22
|
+
const result = await window.fetchJson('GET', 'https://httpbin.org/json')
|
|
23
|
+
|
|
44
24
|
expect(result.status).toBe(200)
|
|
45
25
|
expect(result.statusText).toBe('OK')
|
|
26
|
+
expect(result.data).toBeDefined()
|
|
27
|
+
expect(typeof result.data).toBe('object')
|
|
46
28
|
expect(result.headers).toBeDefined()
|
|
47
29
|
})
|
|
48
30
|
|
|
49
31
|
test('fetches JSON with POST method and body', async () => {
|
|
50
|
-
const
|
|
51
|
-
const requestBody = { name: 'Test' }
|
|
32
|
+
const requestBody = { name: 'Test', value: 123 }
|
|
52
33
|
|
|
53
|
-
|
|
54
|
-
Promise.resolve({
|
|
55
|
-
ok: true,
|
|
56
|
-
status: 201,
|
|
57
|
-
statusText: 'Created',
|
|
58
|
-
headers: new Headers(),
|
|
59
|
-
json: () => Promise.resolve(mockData)
|
|
60
|
-
})
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
const result = await window.fetchJson('POST', 'http://test.com/api', {
|
|
34
|
+
const result = await window.fetchJson('POST', 'https://httpbin.org/post', {
|
|
64
35
|
body: JSON.stringify(requestBody)
|
|
65
36
|
})
|
|
66
37
|
|
|
67
|
-
expect(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
method: 'POST',
|
|
71
|
-
headers: { 'Content-Type': 'application/json' },
|
|
72
|
-
credentials: 'include',
|
|
73
|
-
body: JSON.stringify(requestBody)
|
|
74
|
-
})
|
|
75
|
-
)
|
|
76
|
-
expect(result.data).toEqual(mockData)
|
|
77
|
-
expect(result.status).toBe(201)
|
|
78
|
-
expect(result.statusText).toBe('Created')
|
|
38
|
+
expect(result.status).toBe(200)
|
|
39
|
+
expect(result.data).toBeDefined()
|
|
40
|
+
expect(result.data.json).toEqual(requestBody)
|
|
79
41
|
})
|
|
80
42
|
|
|
81
43
|
test('fetches JSON with PUT method', async () => {
|
|
82
|
-
const
|
|
44
|
+
const requestBody = { name: 'Updated' }
|
|
83
45
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
ok: true,
|
|
87
|
-
status: 200,
|
|
88
|
-
statusText: 'OK',
|
|
89
|
-
headers: new Headers(),
|
|
90
|
-
json: () => Promise.resolve(mockData)
|
|
91
|
-
})
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
const result = await window.fetchJson('PUT', 'http://test.com/api/1', {
|
|
95
|
-
body: JSON.stringify({ name: 'Updated' })
|
|
46
|
+
const result = await window.fetchJson('PUT', 'https://httpbin.org/put', {
|
|
47
|
+
body: JSON.stringify(requestBody)
|
|
96
48
|
})
|
|
97
49
|
|
|
98
|
-
expect(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
method: 'PUT',
|
|
102
|
-
headers: { 'Content-Type': 'application/json' },
|
|
103
|
-
credentials: 'include'
|
|
104
|
-
})
|
|
105
|
-
)
|
|
106
|
-
expect(result.data).toEqual(mockData)
|
|
50
|
+
expect(result.status).toBe(200)
|
|
51
|
+
expect(result.data).toBeDefined()
|
|
52
|
+
expect(result.data.json).toEqual(requestBody)
|
|
107
53
|
})
|
|
108
54
|
|
|
109
55
|
test('fetches JSON with DELETE method', async () => {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
ok: true,
|
|
115
|
-
status: 200,
|
|
116
|
-
statusText: 'OK',
|
|
117
|
-
headers: new Headers(),
|
|
118
|
-
json: () => Promise.resolve(mockData)
|
|
119
|
-
})
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
const result = await window.fetchJson('DELETE', 'http://test.com/api/1')
|
|
123
|
-
|
|
124
|
-
expect(global.fetch).toHaveBeenCalledWith(
|
|
125
|
-
'http://test.com/api/1',
|
|
126
|
-
expect.objectContaining({
|
|
127
|
-
method: 'DELETE',
|
|
128
|
-
headers: { 'Content-Type': 'application/json' },
|
|
129
|
-
credentials: 'include'
|
|
130
|
-
})
|
|
131
|
-
)
|
|
132
|
-
expect(result.data).toEqual(mockData)
|
|
56
|
+
const result = await window.fetchJson('DELETE', 'https://httpbin.org/delete')
|
|
57
|
+
|
|
58
|
+
expect(result.status).toBe(200)
|
|
59
|
+
expect(result.data).toBeDefined()
|
|
133
60
|
})
|
|
134
61
|
|
|
135
|
-
test('
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
ok: true,
|
|
139
|
-
status: 200,
|
|
140
|
-
statusText: 'OK',
|
|
141
|
-
headers: new Headers(),
|
|
142
|
-
json: () => Promise.resolve({})
|
|
143
|
-
})
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
await window.fetchJson('GET', 'http://test.com/api', {
|
|
147
|
-
headers: { 'Authorization': 'Bearer token123' }
|
|
62
|
+
test('includes Content-Type header', async () => {
|
|
63
|
+
const result = await window.fetchJson('POST', 'https://httpbin.org/post', {
|
|
64
|
+
body: JSON.stringify({ test: 'data' })
|
|
148
65
|
})
|
|
149
66
|
|
|
150
|
-
expect(
|
|
151
|
-
|
|
152
|
-
expect.objectContaining({
|
|
153
|
-
headers: {
|
|
154
|
-
'Content-Type': 'application/json'
|
|
155
|
-
}
|
|
156
|
-
})
|
|
157
|
-
)
|
|
158
|
-
// Custom headers are overwritten by Content-Type
|
|
159
|
-
expect(global.fetch).not.toHaveBeenCalledWith(
|
|
160
|
-
expect.anything(),
|
|
161
|
-
expect.objectContaining({
|
|
162
|
-
headers: expect.objectContaining({
|
|
163
|
-
'Authorization': 'Bearer token123'
|
|
164
|
-
})
|
|
165
|
-
})
|
|
166
|
-
)
|
|
67
|
+
expect(result.status).toBe(200)
|
|
68
|
+
expect(result.data.headers['Content-Type']).toBe('application/json')
|
|
167
69
|
})
|
|
168
70
|
|
|
169
|
-
test('
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
headers: new Headers(),
|
|
176
|
-
json: () => Promise.resolve({})
|
|
177
|
-
})
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
await window.fetchJson('GET', 'http://test.com/api')
|
|
181
|
-
|
|
182
|
-
expect(global.fetch).toHaveBeenCalledWith(
|
|
183
|
-
'http://test.com/api',
|
|
184
|
-
expect.objectContaining({
|
|
185
|
-
credentials: 'include'
|
|
186
|
-
})
|
|
187
|
-
)
|
|
71
|
+
test('includes credentials in request', async () => {
|
|
72
|
+
const result = await window.fetchJson('GET', 'https://httpbin.org/get')
|
|
73
|
+
|
|
74
|
+
expect(result.status).toBe(200)
|
|
75
|
+
// httpbin doesn't expose credentials, but we verify the request succeeded
|
|
76
|
+
expect(result.data).toBeDefined()
|
|
188
77
|
})
|
|
189
78
|
|
|
190
79
|
test('handles error responses', async () => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
statusText: 'Not Found',
|
|
196
|
-
headers: new Headers(),
|
|
197
|
-
json: () => Promise.resolve({ error: 'Not found' })
|
|
198
|
-
})
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
const result = await window.fetchJson('GET', 'http://test.com/api')
|
|
202
|
-
|
|
203
|
-
expect(result.status).toBe(404)
|
|
204
|
-
expect(result.statusText).toBe('Not Found')
|
|
205
|
-
expect(result.data).toEqual({ error: 'Not found' })
|
|
80
|
+
// httpbin status endpoints return empty body, so fetchJson will throw on JSON parse
|
|
81
|
+
await expect(
|
|
82
|
+
window.fetchJson('GET', 'https://httpbin.org/status/404')
|
|
83
|
+
).rejects.toThrow()
|
|
206
84
|
})
|
|
207
85
|
|
|
208
86
|
test('handles network errors', async () => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
await expect(window.fetchJson('GET', 'http://test.com/api')).rejects.toThrow('Network error')
|
|
87
|
+
await expect(
|
|
88
|
+
window.fetchJson('GET', 'https://invalid-domain-that-does-not-exist-12345.com/api')
|
|
89
|
+
).rejects.toThrow()
|
|
214
90
|
})
|
|
215
91
|
})
|
|
216
92
|
})
|
|
217
|
-
|
package/components.js
CHANGED
|
@@ -5,117 +5,6 @@ window.components = {
|
|
|
5
5
|
await loadJS('https://cdn.jsdelivr.net/npm/marked/marked.min.js')
|
|
6
6
|
this.innerHTML = marked.parse(this.innerText)
|
|
7
7
|
}
|
|
8
|
-
},
|
|
9
|
-
"auto-complete": {
|
|
10
|
-
init() {
|
|
11
|
-
// Prevent re-initialization
|
|
12
|
-
if (this._initialized) return
|
|
13
|
-
this._initialized = true
|
|
14
|
-
|
|
15
|
-
// Ensure element is visible
|
|
16
|
-
this.style.display = 'block'
|
|
17
|
-
this.style.width = '100%'
|
|
18
|
-
this.style.minWidth = '200px'
|
|
19
|
-
this.style.margin = '10px 0'
|
|
20
|
-
this.style.padding = '0'
|
|
21
|
-
this.style.backgroundColor = 'transparent'
|
|
22
|
-
|
|
23
|
-
this.input = document.createElement('input')
|
|
24
|
-
this.input.type = 'text'
|
|
25
|
-
this.input.placeholder = this.getAttribute('placeholder') || 'Type to search...'
|
|
26
|
-
this.input.style.cssText = 'width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;font-size:14px;'
|
|
27
|
-
|
|
28
|
-
this.dropdown = document.createElement('div')
|
|
29
|
-
this.dropdown.style.cssText = 'position:absolute;top:100%;left:0;right:0;background:white;border:1px solid #ccc;border-top:none;max-height:200px;overflow-y:auto;z-index:1000;display:none;box-shadow:0 2px 4px rgba(0,0,0,0.1);'
|
|
30
|
-
|
|
31
|
-
this.container = document.createElement('div')
|
|
32
|
-
this.container.style.cssText = 'position:relative;width:100%;min-width:200px;'
|
|
33
|
-
this.container.appendChild(this.input)
|
|
34
|
-
this.container.appendChild(this.dropdown)
|
|
35
|
-
|
|
36
|
-
// Clear existing content
|
|
37
|
-
this.innerHTML = ''
|
|
38
|
-
this.appendChild(this.container)
|
|
39
|
-
|
|
40
|
-
this.items = []
|
|
41
|
-
this.filtered = []
|
|
42
|
-
this.selectedIndex = -1
|
|
43
|
-
|
|
44
|
-
this.input.addEventListener('input', () => this.filter())
|
|
45
|
-
this.input.addEventListener('keydown', (e) => this.handleKey(e))
|
|
46
|
-
this.input.addEventListener('focus', () => {
|
|
47
|
-
if (this.filtered.length > 0) this.dropdown.style.display = 'block'
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
// Use a unique handler per instance
|
|
51
|
-
this._clickHandler = (e) => {
|
|
52
|
-
if (!this.contains(e.target)) {
|
|
53
|
-
this.dropdown.style.display = 'none'
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
document.addEventListener('click', this._clickHandler)
|
|
57
|
-
},
|
|
58
|
-
set(data) {
|
|
59
|
-
this.items = Array.isArray(data) ? data : (data?.items || [])
|
|
60
|
-
this.filter()
|
|
61
|
-
},
|
|
62
|
-
filter() {
|
|
63
|
-
const query = this.input.value.toLowerCase()
|
|
64
|
-
this.filtered = this.items.filter(item => {
|
|
65
|
-
const text = typeof item === 'string' ? item : (item.label || item.name || String(item))
|
|
66
|
-
return text.toLowerCase().includes(query)
|
|
67
|
-
})
|
|
68
|
-
this.render()
|
|
69
|
-
},
|
|
70
|
-
render() {
|
|
71
|
-
if (this.filtered.length === 0) {
|
|
72
|
-
this.dropdown.style.display = 'none'
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
this.dropdown.innerHTML = this.filtered.map((item, i) => {
|
|
77
|
-
const text = typeof item === 'string' ? item : (item.label || item.name || String(item))
|
|
78
|
-
const selected = i === this.selectedIndex ? 'background:#f0f0f0;' : ''
|
|
79
|
-
return `<div data-index="${i}" style="padding:8px;cursor:pointer;${selected}">${text}</div>`
|
|
80
|
-
}).join('')
|
|
81
|
-
|
|
82
|
-
this.dropdown.style.display = 'block'
|
|
83
|
-
|
|
84
|
-
this.dropdown.querySelectorAll('div').forEach((div, i) => {
|
|
85
|
-
div.addEventListener('click', () => this.select(i))
|
|
86
|
-
div.addEventListener('mouseenter', () => {
|
|
87
|
-
this.selectedIndex = i
|
|
88
|
-
this.render()
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
},
|
|
92
|
-
select(index) {
|
|
93
|
-
const item = this.filtered[index]
|
|
94
|
-
const text = typeof item === 'string' ? item : (item.label || item.name || String(item))
|
|
95
|
-
this.input.value = text
|
|
96
|
-
this.dropdown.style.display = 'none'
|
|
97
|
-
this.selectedIndex = -1
|
|
98
|
-
|
|
99
|
-
const event = new CustomEvent('select', { detail: item })
|
|
100
|
-
this.dispatchEvent(event)
|
|
101
|
-
},
|
|
102
|
-
handleKey(e) {
|
|
103
|
-
if (e.key === 'ArrowDown') {
|
|
104
|
-
e.preventDefault()
|
|
105
|
-
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filtered.length - 1)
|
|
106
|
-
this.render()
|
|
107
|
-
} else if (e.key === 'ArrowUp') {
|
|
108
|
-
e.preventDefault()
|
|
109
|
-
this.selectedIndex = Math.max(this.selectedIndex - 1, -1)
|
|
110
|
-
this.render()
|
|
111
|
-
} else if (e.key === 'Enter' && this.selectedIndex >= 0) {
|
|
112
|
-
e.preventDefault()
|
|
113
|
-
this.select(this.selectedIndex)
|
|
114
|
-
} else if (e.key === 'Escape') {
|
|
115
|
-
this.dropdown.style.display = 'none'
|
|
116
|
-
this.selectedIndex = -1
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
8
|
}
|
|
120
9
|
}
|
|
121
10
|
|
package/jest.config.js
CHANGED
package/jest.setup.js
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enigmatic",
|
|
3
3
|
"main": "enigmatic.js",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.27.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "jest",
|
|
7
7
|
"test:watch": "jest --watch"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"jest": "^29.7.0",
|
|
11
|
-
"jest-environment-jsdom": "^29.7.0"
|
|
11
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
12
|
+
"whatwg-fetch": "^3.6.20"
|
|
12
13
|
}
|
|
13
14
|
}
|