@daz4126/helium 0.30.0 → 0.31.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/.claude/settings.local.json +15 -0
- package/CLAUDE.md +137 -0
- package/README.md +46 -18
- package/examples/index.html +485 -0
- package/examples/sse-demo.html +125 -0
- package/examples/sse-server.js +138 -0
- package/examples/sse-simple.html +49 -0
- package/examples/test-password.html +251 -0
- package/helium.js +568 -337
- package/helium.test.js +562 -212
- package/package.json +6 -2
package/helium.test.js
CHANGED
|
@@ -11,287 +11,308 @@ describe('Helium Reactive Library', () => {
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
afterEach(() => {
|
|
14
|
+
window.heliumTeardown?.();
|
|
14
15
|
document.body.innerHTML = '';
|
|
15
16
|
});
|
|
16
17
|
|
|
17
18
|
describe('State Reactivity', () => {
|
|
18
|
-
it('should initialize with default data', () => {
|
|
19
|
+
it('should initialize with default data', async () => {
|
|
19
20
|
container.innerHTML = '<div @text="count"></div>';
|
|
20
|
-
helium({ count: 5 });
|
|
21
|
+
await helium({ count: 5 });
|
|
21
22
|
expect(container.querySelector('div').textContent).toBe('5');
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
it('should react to direct property assignment', (
|
|
25
|
+
it('should react to direct property assignment', async () => {
|
|
25
26
|
container.innerHTML = '<div @text="count"></div>';
|
|
26
|
-
const state = helium({ count: 0 });
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}, 0);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should react to nested object mutations', (done) => {
|
|
27
|
+
const state = await helium({ count: 0 });
|
|
28
|
+
|
|
29
|
+
state.count = 10;
|
|
30
|
+
await new Promise(r => setTimeout(r, 0));
|
|
31
|
+
expect(container.querySelector('div').textContent).toBe('10');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should react to nested object mutations', async () => {
|
|
38
35
|
container.innerHTML = '<div @text="user.name"></div>';
|
|
39
|
-
const state = helium({ user: { name: 'Alice' } });
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}, 0);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should react to array push', (done) => {
|
|
36
|
+
const state = await helium({ user: { name: 'Alice' } });
|
|
37
|
+
|
|
38
|
+
state.user.name = 'Bob';
|
|
39
|
+
await new Promise(r => setTimeout(r, 0));
|
|
40
|
+
expect(container.querySelector('div').textContent).toBe('Bob');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should react to array push', async () => {
|
|
51
44
|
container.innerHTML = '<div @text="items.length"></div>';
|
|
52
|
-
const state = helium({ items: [1, 2, 3] });
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}, 0);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should react to array index assignment', (done) => {
|
|
45
|
+
const state = await helium({ items: [1, 2, 3] });
|
|
46
|
+
|
|
47
|
+
state.items.push(4);
|
|
48
|
+
await new Promise(r => setTimeout(r, 0));
|
|
49
|
+
expect(container.querySelector('div').textContent).toBe('4');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should react to array index assignment', async () => {
|
|
64
53
|
container.innerHTML = '<div @text="items[0]"></div>';
|
|
65
|
-
const state = helium({ items: ['a', 'b', 'c'] });
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}, 0);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should react to complete array replacement', (done) => {
|
|
54
|
+
const state = await helium({ items: ['a', 'b', 'c'] });
|
|
55
|
+
|
|
56
|
+
state.items[0] = 'z';
|
|
57
|
+
await new Promise(r => setTimeout(r, 0));
|
|
58
|
+
expect(container.querySelector('div').textContent).toBe('z');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should react to complete array replacement', async () => {
|
|
77
62
|
container.innerHTML = '<div @text="items.length"></div>';
|
|
78
|
-
const state = helium({ items: [1, 2, 3] });
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
expect(container.querySelector('div').textContent).toBe('5');
|
|
84
|
-
done();
|
|
85
|
-
}, 0);
|
|
86
|
-
}, 0);
|
|
63
|
+
const state = await helium({ items: [1, 2, 3] });
|
|
64
|
+
|
|
65
|
+
state.items = [...state.items, 4, 5];
|
|
66
|
+
await new Promise(r => setTimeout(r, 0));
|
|
67
|
+
expect(container.querySelector('div').textContent).toBe('5');
|
|
87
68
|
});
|
|
88
69
|
});
|
|
89
70
|
|
|
90
71
|
describe('Directives - @text and @html', () => {
|
|
91
|
-
it('should bind text content with @text', () => {
|
|
72
|
+
it('should bind text content with @text', async () => {
|
|
92
73
|
container.innerHTML = '<div @text="message"></div>';
|
|
93
|
-
helium({ message: 'Hello World' });
|
|
74
|
+
await helium({ message: 'Hello World' });
|
|
94
75
|
expect(container.querySelector('div').textContent).toBe('Hello World');
|
|
95
76
|
});
|
|
96
77
|
|
|
97
|
-
it('should bind HTML content with @html', () => {
|
|
78
|
+
it('should bind HTML content with @html', async () => {
|
|
98
79
|
container.innerHTML = '<div @html="content"></div>';
|
|
99
|
-
helium({ content: '<strong>Bold</strong>' });
|
|
80
|
+
await helium({ content: '<strong>Bold</strong>' });
|
|
100
81
|
expect(container.querySelector('div').innerHTML).toBe('<strong>Bold</strong>');
|
|
101
82
|
});
|
|
102
83
|
|
|
103
|
-
it('should work with data-he-text prefix', () => {
|
|
84
|
+
it('should work with data-he-text prefix', async () => {
|
|
104
85
|
container.innerHTML = '<div data-he-text="message"></div>';
|
|
105
|
-
helium({ message: 'Test' });
|
|
86
|
+
await helium({ message: 'Test' });
|
|
106
87
|
expect(container.querySelector('div').textContent).toBe('Test');
|
|
107
88
|
});
|
|
108
89
|
|
|
109
|
-
it('should evaluate expressions in @text', () => {
|
|
90
|
+
it('should evaluate expressions in @text', async () => {
|
|
110
91
|
container.innerHTML = '<div @text="count * 2"></div>';
|
|
111
|
-
helium({ count: 5 });
|
|
92
|
+
await helium({ count: 5 });
|
|
112
93
|
expect(container.querySelector('div').textContent).toBe('10');
|
|
113
94
|
});
|
|
114
95
|
});
|
|
115
96
|
|
|
116
97
|
describe('Directives - @bind', () => {
|
|
117
|
-
it('should create two-way binding with input', () => {
|
|
98
|
+
it('should create two-way binding with input', async () => {
|
|
118
99
|
container.innerHTML = '<input @bind="name" />';
|
|
119
|
-
|
|
100
|
+
await helium({ name: 'John' });
|
|
120
101
|
const input = container.querySelector('input');
|
|
121
|
-
|
|
102
|
+
|
|
122
103
|
expect(input.value).toBe('John');
|
|
123
104
|
});
|
|
124
105
|
|
|
125
|
-
it('should update state when input changes', (
|
|
106
|
+
it('should update state when input changes', async () => {
|
|
126
107
|
container.innerHTML = '<input @bind="name" /><span @text="name"></span>';
|
|
127
|
-
|
|
108
|
+
await helium({ name: 'John' });
|
|
128
109
|
const input = container.querySelector('input');
|
|
129
|
-
|
|
110
|
+
|
|
130
111
|
input.value = 'Jane';
|
|
131
112
|
input.dispatchEvent(new Event('input'));
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
done();
|
|
136
|
-
}, 0);
|
|
113
|
+
|
|
114
|
+
await new Promise(r => setTimeout(r, 0));
|
|
115
|
+
expect(container.querySelector('span').textContent).toBe('Jane');
|
|
137
116
|
});
|
|
138
117
|
});
|
|
139
118
|
|
|
140
119
|
describe('Directives - @hidden and @visible', () => {
|
|
141
|
-
it('should hide element with @hidden when true', () => {
|
|
120
|
+
it('should hide element with @hidden when true', async () => {
|
|
142
121
|
container.innerHTML = '<div @hidden="isHidden"></div>';
|
|
143
|
-
helium({ isHidden: true });
|
|
122
|
+
await helium({ isHidden: true });
|
|
144
123
|
expect(container.querySelector('div').hidden).toBe(true);
|
|
145
124
|
});
|
|
146
125
|
|
|
147
|
-
it('should show element with @hidden when false', () => {
|
|
126
|
+
it('should show element with @hidden when false', async () => {
|
|
148
127
|
container.innerHTML = '<div @hidden="isHidden"></div>';
|
|
149
|
-
helium({ isHidden: false });
|
|
128
|
+
await helium({ isHidden: false });
|
|
150
129
|
expect(container.querySelector('div').hidden).toBe(false);
|
|
151
130
|
});
|
|
152
131
|
|
|
153
|
-
it('should show element with @visible when true', () => {
|
|
132
|
+
it('should show element with @visible when true', async () => {
|
|
154
133
|
container.innerHTML = '<div @visible="isVisible"></div>';
|
|
155
|
-
helium({ isVisible: true });
|
|
134
|
+
await helium({ isVisible: true });
|
|
156
135
|
expect(container.querySelector('div').hidden).toBe(false);
|
|
157
136
|
});
|
|
158
137
|
|
|
159
|
-
it('should hide element with @visible when false', () => {
|
|
138
|
+
it('should hide element with @visible when false', async () => {
|
|
160
139
|
container.innerHTML = '<div @visible="isVisible"></div>';
|
|
161
|
-
helium({ isVisible: false });
|
|
140
|
+
await helium({ isVisible: false });
|
|
162
141
|
expect(container.querySelector('div').hidden).toBe(true);
|
|
163
142
|
});
|
|
164
143
|
});
|
|
165
144
|
|
|
166
145
|
describe('Directives - Dynamic Attributes (:attr)', () => {
|
|
167
|
-
it('should bind attribute dynamically', () => {
|
|
146
|
+
it('should bind attribute dynamically', async () => {
|
|
168
147
|
container.innerHTML = '<div :class="className"></div>';
|
|
169
|
-
helium({ className: 'active' });
|
|
148
|
+
await helium({ className: 'active' });
|
|
170
149
|
expect(container.querySelector('div').getAttribute('class')).toBe('active');
|
|
171
150
|
});
|
|
172
151
|
|
|
173
|
-
it('should update attribute on state change', (
|
|
152
|
+
it('should update attribute on state change', async () => {
|
|
174
153
|
container.innerHTML = '<div :title="tooltip"></div>';
|
|
175
|
-
const state = helium({ tooltip: 'Initial' });
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
expect(container.querySelector('div').getAttribute('title')).toBe('Updated');
|
|
181
|
-
done();
|
|
182
|
-
}, 0);
|
|
183
|
-
}, 0);
|
|
154
|
+
const state = await helium({ tooltip: 'Initial' });
|
|
155
|
+
|
|
156
|
+
state.tooltip = 'Updated';
|
|
157
|
+
await new Promise(r => setTimeout(r, 0));
|
|
158
|
+
expect(container.querySelector('div').getAttribute('title')).toBe('Updated');
|
|
184
159
|
});
|
|
185
160
|
});
|
|
186
161
|
|
|
187
162
|
describe('Event Handlers', () => {
|
|
188
|
-
it('should handle click events', () => {
|
|
163
|
+
it('should handle click events', async () => {
|
|
189
164
|
container.innerHTML = '<button @click="count++">Click</button><span @text="count"></span>';
|
|
190
|
-
helium({ count: 0 });
|
|
191
|
-
|
|
165
|
+
await helium({ count: 0 });
|
|
166
|
+
|
|
192
167
|
container.querySelector('button').click();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}, 0);
|
|
168
|
+
await new Promise(r => setTimeout(r, 0));
|
|
169
|
+
expect(container.querySelector('span').textContent).toBe('1');
|
|
196
170
|
});
|
|
197
171
|
|
|
198
|
-
it('should handle event with prevent modifier', () => {
|
|
172
|
+
it('should handle event with prevent modifier', async () => {
|
|
199
173
|
container.innerHTML = '<form @submit.prevent="submitted = true"></form>';
|
|
200
|
-
|
|
174
|
+
await helium({ submitted: false });
|
|
201
175
|
const form = container.querySelector('form');
|
|
202
176
|
const event = new Event('submit', { cancelable: true });
|
|
203
|
-
|
|
177
|
+
|
|
204
178
|
form.dispatchEvent(event);
|
|
205
179
|
expect(event.defaultPrevented).toBe(true);
|
|
206
180
|
});
|
|
207
181
|
|
|
208
|
-
it('should handle keyboard events with key modifiers', () => {
|
|
182
|
+
it('should handle keyboard events with key modifiers', async () => {
|
|
209
183
|
container.innerHTML = '<input @keydown.enter="pressed = true" />';
|
|
210
|
-
const state = helium({ pressed: false });
|
|
184
|
+
const state = await helium({ pressed: false });
|
|
211
185
|
const input = container.querySelector('input');
|
|
212
|
-
|
|
186
|
+
|
|
213
187
|
const event = new KeyboardEvent('keydown', { key: 'Enter' });
|
|
214
188
|
input.dispatchEvent(event);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}, 0);
|
|
189
|
+
|
|
190
|
+
await new Promise(r => setTimeout(r, 0));
|
|
191
|
+
expect(state.pressed).toBe(true);
|
|
219
192
|
});
|
|
220
193
|
|
|
221
|
-
it('should handle debounced events', (
|
|
194
|
+
it('should handle debounced events', async () => {
|
|
222
195
|
container.innerHTML = '<input @input.debounce:100="value = $event.target.value" />';
|
|
223
|
-
const state = helium({ value: '' });
|
|
196
|
+
const state = await helium({ value: '' });
|
|
224
197
|
const input = container.querySelector('input');
|
|
225
|
-
|
|
198
|
+
|
|
226
199
|
input.value = 'test';
|
|
227
200
|
input.dispatchEvent(new Event('input'));
|
|
228
|
-
|
|
201
|
+
|
|
229
202
|
// Should not update immediately
|
|
230
203
|
expect(state.value).toBe('');
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
done();
|
|
235
|
-
}, 150);
|
|
204
|
+
|
|
205
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
206
|
+
expect(state.value).toBe('test');
|
|
236
207
|
});
|
|
237
208
|
|
|
238
|
-
it('should handle once modifier', () => {
|
|
209
|
+
it('should handle once modifier', async () => {
|
|
239
210
|
container.innerHTML = '<button @click.once="count++">Click</button>';
|
|
240
|
-
const state = helium({ count: 0 });
|
|
211
|
+
const state = await helium({ count: 0 });
|
|
241
212
|
const button = container.querySelector('button');
|
|
242
|
-
|
|
213
|
+
|
|
243
214
|
button.click();
|
|
244
215
|
button.click();
|
|
245
216
|
button.click();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}, 0);
|
|
217
|
+
|
|
218
|
+
await new Promise(r => setTimeout(r, 0));
|
|
219
|
+
expect(state.count).toBe(1);
|
|
250
220
|
});
|
|
251
221
|
});
|
|
252
222
|
|
|
253
223
|
describe('@ref Directive', () => {
|
|
254
|
-
it('should create reference to element', () => {
|
|
224
|
+
it('should create reference to element', async () => {
|
|
255
225
|
container.innerHTML = '<div @ref="myDiv"></div><button @click="$myDiv.textContent = \'Changed\'">Change</button>';
|
|
256
|
-
helium({});
|
|
257
|
-
|
|
226
|
+
await helium({});
|
|
227
|
+
|
|
228
|
+
container.querySelector('button').click();
|
|
229
|
+
await new Promise(r => setTimeout(r, 0));
|
|
230
|
+
expect(container.querySelector('div').textContent).toBe('Changed');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should make refs available in @params for AJAX requests', async () => {
|
|
234
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
235
|
+
headers: { get: () => 'application/json' },
|
|
236
|
+
json: async () => ({ success: true })
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
container.innerHTML = `
|
|
240
|
+
<form @ref="myForm">
|
|
241
|
+
<input name="title" value="Test Title">
|
|
242
|
+
<button @post="/api/test" @params="new FormData($myForm)">Submit</button>
|
|
243
|
+
</form>
|
|
244
|
+
`;
|
|
245
|
+
await helium({});
|
|
246
|
+
|
|
247
|
+
container.querySelector('button').click();
|
|
248
|
+
await new Promise(r => setTimeout(r, 100));
|
|
249
|
+
|
|
250
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
251
|
+
const [url, options] = global.fetch.mock.calls[0];
|
|
252
|
+
expect(url).toBe('/api/test');
|
|
253
|
+
expect(options.body).toBeInstanceOf(FormData);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should make refs available with nested @data sections', async () => {
|
|
257
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
258
|
+
headers: { get: () => 'application/json' },
|
|
259
|
+
json: async () => ({ success: true })
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Mimic the examples page structure with nested @data
|
|
263
|
+
container.innerHTML = `
|
|
264
|
+
<div @data="{ count: 0 }">
|
|
265
|
+
<div @data="{ templates: { post: data => data.title } }">
|
|
266
|
+
<section @ref="posts">
|
|
267
|
+
<form @ref="form">
|
|
268
|
+
<input name="title" value="Test">
|
|
269
|
+
<button @post.prevent="/api/posts" @params="new FormData($form)" @target="$posts:append" @template="templates.post">Submit</button>
|
|
270
|
+
</form>
|
|
271
|
+
</section>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
`;
|
|
275
|
+
await helium({});
|
|
276
|
+
|
|
258
277
|
container.querySelector('button').click();
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
278
|
+
await new Promise(r => setTimeout(r, 100));
|
|
279
|
+
|
|
280
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
281
|
+
const [url, options] = global.fetch.mock.calls[0];
|
|
282
|
+
expect(url).toBe('/api/posts');
|
|
283
|
+
expect(options.body).toBeInstanceOf(FormData);
|
|
262
284
|
});
|
|
263
285
|
});
|
|
264
286
|
|
|
265
287
|
describe('@init Directive', () => {
|
|
266
|
-
it('should execute code on initialization', () => {
|
|
288
|
+
it('should execute code on initialization', async () => {
|
|
267
289
|
container.innerHTML = '<div @init="initialized = true"></div>';
|
|
268
|
-
const state = helium({ initialized: false });
|
|
290
|
+
const state = await helium({ initialized: false });
|
|
269
291
|
expect(state.initialized).toBe(true);
|
|
270
292
|
});
|
|
271
293
|
});
|
|
272
294
|
|
|
273
295
|
describe('@data Directive', () => {
|
|
274
|
-
it('should merge data into state', () => {
|
|
296
|
+
it('should merge data into state', async () => {
|
|
275
297
|
container.innerHTML = '<div @data="{ user: \'Alice\', age: 30 }"></div><span @text="user"></span>';
|
|
276
|
-
helium({});
|
|
298
|
+
await helium({});
|
|
277
299
|
expect(container.querySelector('span').textContent).toBe('Alice');
|
|
278
300
|
});
|
|
279
301
|
});
|
|
280
302
|
|
|
281
303
|
describe('Helper Functions', () => {
|
|
282
|
-
it('should provide $ selector helper', () => {
|
|
304
|
+
it('should provide $ selector helper', async () => {
|
|
283
305
|
container.innerHTML = '<div id="target"></div><button @click="$(\'#target\').textContent = \'Found\'">Click</button>';
|
|
284
|
-
helium({});
|
|
285
|
-
|
|
306
|
+
await helium({});
|
|
307
|
+
|
|
286
308
|
container.querySelector('button').click();
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}, 0);
|
|
309
|
+
await new Promise(r => setTimeout(r, 0));
|
|
310
|
+
expect(container.querySelector('#target').textContent).toBe('Found');
|
|
290
311
|
});
|
|
291
312
|
|
|
292
|
-
it('should provide $html helper to create elements', () => {
|
|
313
|
+
it('should provide $html helper to create elements', async () => {
|
|
293
314
|
container.innerHTML = '<div @init="$el.appendChild($html(\'<span>Test</span>\'))"></div>';
|
|
294
|
-
helium({});
|
|
315
|
+
await helium({});
|
|
295
316
|
expect(container.querySelector('span').textContent).toBe('Test');
|
|
296
317
|
});
|
|
297
318
|
});
|
|
@@ -308,10 +329,10 @@ describe('Helium Reactive Library', () => {
|
|
|
308
329
|
});
|
|
309
330
|
|
|
310
331
|
container.innerHTML = '<button @click="$get(\'/api/data\', \'result\')">Load</button><div @text="result?.data"></div>';
|
|
311
|
-
helium({ result: null });
|
|
312
|
-
|
|
332
|
+
await helium({ result: null });
|
|
333
|
+
|
|
313
334
|
container.querySelector('button').click();
|
|
314
|
-
|
|
335
|
+
|
|
315
336
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
316
337
|
expect(global.fetch).toHaveBeenCalledWith('/api/data', expect.any(Object));
|
|
317
338
|
});
|
|
@@ -323,10 +344,10 @@ describe('Helium Reactive Library', () => {
|
|
|
323
344
|
});
|
|
324
345
|
|
|
325
346
|
container.innerHTML = '<button @click="$post(\'/api/save\', { name: \'test\' }, \'response\')">Save</button>';
|
|
326
|
-
helium({ response: null });
|
|
327
|
-
|
|
347
|
+
await helium({ response: null });
|
|
348
|
+
|
|
328
349
|
container.querySelector('button').click();
|
|
329
|
-
|
|
350
|
+
|
|
330
351
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
331
352
|
expect(global.fetch).toHaveBeenCalledWith('/api/save', expect.objectContaining({
|
|
332
353
|
method: 'POST',
|
|
@@ -335,107 +356,436 @@ describe('Helium Reactive Library', () => {
|
|
|
335
356
|
});
|
|
336
357
|
});
|
|
337
358
|
|
|
359
|
+
describe('Server-Sent Events (SSE)', () => {
|
|
360
|
+
// Helper to create a mock SSE ReadableStream
|
|
361
|
+
const createSSEStream = (events) => {
|
|
362
|
+
const encoder = new TextEncoder();
|
|
363
|
+
let index = 0;
|
|
364
|
+
return new ReadableStream({
|
|
365
|
+
pull(controller) {
|
|
366
|
+
if (index < events.length) {
|
|
367
|
+
controller.enqueue(encoder.encode(events[index]));
|
|
368
|
+
index++;
|
|
369
|
+
} else {
|
|
370
|
+
controller.close();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
beforeEach(() => {
|
|
377
|
+
global.fetch = vi.fn();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should handle basic SSE stream updating DOM target', async () => {
|
|
381
|
+
// Simulate SSE response like Datastar hello-world example
|
|
382
|
+
const sseData = 'event: #message\ndata: <div>Hello from SSE!</div>\n\n';
|
|
383
|
+
|
|
384
|
+
global.fetch.mockResolvedValueOnce({
|
|
385
|
+
headers: { get: () => 'text/event-stream' },
|
|
386
|
+
body: createSSEStream([sseData])
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
container.innerHTML = `
|
|
390
|
+
<button @get="/api/hello-world" @target="#message">Start</button>
|
|
391
|
+
<div id="message"></div>
|
|
392
|
+
`;
|
|
393
|
+
await helium({});
|
|
394
|
+
|
|
395
|
+
container.querySelector('button').click();
|
|
396
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
397
|
+
|
|
398
|
+
expect(container.querySelector('#message').innerHTML).toBe('<div>Hello from SSE!</div>');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should handle multiple SSE events in sequence', async () => {
|
|
402
|
+
const sseData = [
|
|
403
|
+
'event: #counter\ndata: <span>1</span>\n\n',
|
|
404
|
+
'event: #counter\ndata: <span>2</span>\n\n',
|
|
405
|
+
'event: #counter\ndata: <span>3</span>\n\n'
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
global.fetch.mockResolvedValueOnce({
|
|
409
|
+
headers: { get: () => 'text/event-stream' },
|
|
410
|
+
body: createSSEStream(sseData)
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
container.innerHTML = `
|
|
414
|
+
<button @get="/api/count" @target="#counter">Count</button>
|
|
415
|
+
<div id="counter"></div>
|
|
416
|
+
`;
|
|
417
|
+
await helium({});
|
|
418
|
+
|
|
419
|
+
container.querySelector('button').click();
|
|
420
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
421
|
+
|
|
422
|
+
// Should have the last value after all events processed
|
|
423
|
+
expect(container.querySelector('#counter').innerHTML).toBe('<span>3</span>');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should update state when event targets a state property', async () => {
|
|
427
|
+
const sseData = 'event: message\ndata: Hello World\n\n';
|
|
428
|
+
|
|
429
|
+
global.fetch.mockResolvedValueOnce({
|
|
430
|
+
headers: { get: () => 'text/event-stream' },
|
|
431
|
+
body: createSSEStream([sseData])
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
container.innerHTML = `
|
|
435
|
+
<button @get="/api/message" @target="message">Load</button>
|
|
436
|
+
<span @text="message"></span>
|
|
437
|
+
`;
|
|
438
|
+
const state = await helium({ message: '' });
|
|
439
|
+
|
|
440
|
+
container.querySelector('button').click();
|
|
441
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
442
|
+
|
|
443
|
+
expect(state.message).toBe('Hello World');
|
|
444
|
+
expect(container.querySelector('span').textContent).toBe('Hello World');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should handle multiline data in SSE events', async () => {
|
|
448
|
+
const sseData = 'data: line 1\ndata: line 2\ndata: line 3\n\n';
|
|
449
|
+
|
|
450
|
+
global.fetch.mockResolvedValueOnce({
|
|
451
|
+
headers: { get: () => 'text/event-stream' },
|
|
452
|
+
body: createSSEStream([sseData])
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
container.innerHTML = `
|
|
456
|
+
<button @get="/api/multiline" @target="#output">Load</button>
|
|
457
|
+
<pre id="output"></pre>
|
|
458
|
+
`;
|
|
459
|
+
await helium({});
|
|
460
|
+
|
|
461
|
+
container.querySelector('button').click();
|
|
462
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
463
|
+
|
|
464
|
+
expect(container.querySelector('#output').innerHTML).toBe('line 1\nline 2\nline 3');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should use @target for SSE updates', async () => {
|
|
468
|
+
const sseData = 'data: <div>SSE content</div>\n\n';
|
|
469
|
+
|
|
470
|
+
global.fetch.mockResolvedValueOnce({
|
|
471
|
+
headers: { get: () => 'text/event-stream' },
|
|
472
|
+
body: createSSEStream([sseData])
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
container.innerHTML = `
|
|
476
|
+
<button @get="/api/simple" @target="#result">Load</button>
|
|
477
|
+
<div id="result"></div>
|
|
478
|
+
`;
|
|
479
|
+
await helium({});
|
|
480
|
+
|
|
481
|
+
container.querySelector('button').click();
|
|
482
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
483
|
+
|
|
484
|
+
expect(container.querySelector('#result').innerHTML).toBe('<div>SSE content</div>');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should track event id for potential reconnection', async () => {
|
|
488
|
+
const sseData = 'id: 42\ndata: <span>Connected</span>\n\n';
|
|
489
|
+
|
|
490
|
+
global.fetch.mockResolvedValueOnce({
|
|
491
|
+
headers: { get: () => 'text/event-stream' },
|
|
492
|
+
body: createSSEStream([sseData])
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
container.innerHTML = `
|
|
496
|
+
<button @get="/api/status" @target="#status">Connect</button>
|
|
497
|
+
<div id="status"></div>
|
|
498
|
+
`;
|
|
499
|
+
await helium({});
|
|
500
|
+
|
|
501
|
+
container.querySelector('button').click();
|
|
502
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
503
|
+
|
|
504
|
+
expect(container.querySelector('#status').innerHTML).toBe('<span>Connected</span>');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should handle chunked SSE data', async () => {
|
|
508
|
+
// Simulate data arriving in chunks (like real network)
|
|
509
|
+
const chunk1 = 'event: #result\nda';
|
|
510
|
+
const chunk2 = 'ta: <div>Chunked!</div>\n\n';
|
|
511
|
+
|
|
512
|
+
global.fetch.mockResolvedValueOnce({
|
|
513
|
+
headers: { get: () => 'text/event-stream' },
|
|
514
|
+
body: createSSEStream([chunk1, chunk2])
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
container.innerHTML = `
|
|
518
|
+
<button @get="/api/chunked" @target="#result">Load</button>
|
|
519
|
+
<div id="result"></div>
|
|
520
|
+
`;
|
|
521
|
+
await helium({});
|
|
522
|
+
|
|
523
|
+
container.querySelector('button').click();
|
|
524
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
525
|
+
|
|
526
|
+
expect(container.querySelector('#result').innerHTML).toBe('<div>Chunked!</div>');
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
338
530
|
describe('Dynamic DOM Updates', () => {
|
|
339
|
-
it('should process dynamically added elements', (
|
|
531
|
+
it('should process dynamically added elements', async () => {
|
|
340
532
|
container.innerHTML = '<div id="container"></div>';
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}, 0);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it('should handle innerHTML updates with new bindings', (done) => {
|
|
533
|
+
await helium({ message: 'Dynamic' });
|
|
534
|
+
|
|
535
|
+
const newEl = document.createElement('span');
|
|
536
|
+
newEl.setAttribute('data-he-text', 'message');
|
|
537
|
+
container.querySelector('#container').appendChild(newEl);
|
|
538
|
+
|
|
539
|
+
await new Promise(r => setTimeout(r, 100));
|
|
540
|
+
expect(newEl.textContent).toBe('Dynamic');
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should handle innerHTML updates with new bindings', async () => {
|
|
356
544
|
container.innerHTML = '<div @html="content"></div>';
|
|
357
|
-
const state = helium({ content: '', name: 'Test' });
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
expect(container.querySelector('span').textContent).toBe('Test');
|
|
363
|
-
done();
|
|
364
|
-
}, 100);
|
|
365
|
-
}, 0);
|
|
545
|
+
const state = await helium({ content: '', name: 'Test' });
|
|
546
|
+
|
|
547
|
+
state.content = '<span data-he-text="name"></span>';
|
|
548
|
+
await new Promise(r => setTimeout(r, 100));
|
|
549
|
+
expect(container.querySelector('span').textContent).toBe('Test');
|
|
366
550
|
});
|
|
367
551
|
});
|
|
368
552
|
|
|
369
553
|
describe('Edge Cases', () => {
|
|
370
|
-
it('should handle undefined state values', () => {
|
|
554
|
+
it('should handle undefined state values', async () => {
|
|
371
555
|
container.innerHTML = '<div @text="undefined"></div>';
|
|
372
|
-
helium({});
|
|
373
|
-
expect(container.querySelector('div').textContent).toBe('
|
|
556
|
+
await helium({});
|
|
557
|
+
expect(container.querySelector('div').textContent).toBe('');
|
|
374
558
|
});
|
|
375
559
|
|
|
376
|
-
it('should handle null state values', () => {
|
|
560
|
+
it('should handle null state values', async () => {
|
|
377
561
|
container.innerHTML = '<div @text="value"></div>';
|
|
378
|
-
helium({ value: null });
|
|
379
|
-
expect(container.querySelector('div').textContent).toBe('
|
|
562
|
+
await helium({ value: null });
|
|
563
|
+
expect(container.querySelector('div').textContent).toBe('');
|
|
380
564
|
});
|
|
381
565
|
|
|
382
566
|
it('should handle circular references in state', () => {
|
|
383
567
|
const obj = { name: 'test' };
|
|
384
568
|
obj.self = obj;
|
|
385
|
-
|
|
569
|
+
|
|
386
570
|
container.innerHTML = '<div @text="obj.name"></div>';
|
|
387
571
|
expect(() => helium({ obj })).not.toThrow();
|
|
388
572
|
});
|
|
389
573
|
|
|
390
|
-
it('should not process already processed elements', () => {
|
|
574
|
+
it('should not process already processed elements', async () => {
|
|
391
575
|
container.innerHTML = '<div @text="count"></div>';
|
|
392
|
-
const state = helium({ count: 0 });
|
|
393
|
-
|
|
576
|
+
const state = await helium({ count: 0 });
|
|
577
|
+
|
|
394
578
|
// Manually mark as processed
|
|
395
579
|
container.querySelector('div').setAttribute('data-helium-processed', '');
|
|
396
|
-
|
|
580
|
+
|
|
397
581
|
state.count = 5;
|
|
398
582
|
// Should still work because binding was already created
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}, 0);
|
|
583
|
+
await new Promise(r => setTimeout(r, 0));
|
|
584
|
+
expect(container.querySelector('div').textContent).toBe('5');
|
|
402
585
|
});
|
|
403
586
|
});
|
|
404
587
|
|
|
405
588
|
describe('Performance', () => {
|
|
406
|
-
it('should handle many bindings efficiently', () => {
|
|
407
|
-
const html = Array.from({ length: 100 }, (_, i) =>
|
|
589
|
+
it('should handle many bindings efficiently', async () => {
|
|
590
|
+
const html = Array.from({ length: 100 }, (_, i) =>
|
|
408
591
|
`<div @text="items[${i}]"></div>`
|
|
409
592
|
).join('');
|
|
410
|
-
|
|
593
|
+
|
|
411
594
|
container.innerHTML = html;
|
|
412
595
|
const items = Array.from({ length: 100 }, (_, i) => i);
|
|
413
|
-
|
|
596
|
+
|
|
414
597
|
const start = performance.now();
|
|
415
|
-
helium({ items });
|
|
598
|
+
await helium({ items });
|
|
416
599
|
const end = performance.now();
|
|
417
|
-
|
|
600
|
+
|
|
418
601
|
expect(end - start).toBeLessThan(100); // Should initialize in < 100ms
|
|
419
602
|
});
|
|
420
603
|
|
|
421
|
-
it('should not cause infinite loops with DOM mutations', (
|
|
604
|
+
it('should not cause infinite loops with DOM mutations', async () => {
|
|
422
605
|
container.innerHTML = '<div @html="content"></div>';
|
|
423
|
-
const state = helium({ content: '<span>Test</span>', updateCount: 0 });
|
|
424
|
-
|
|
606
|
+
const state = await helium({ content: '<span>Test</span>', updateCount: 0 });
|
|
607
|
+
|
|
425
608
|
let mutationCount = 0;
|
|
426
609
|
const observer = new MutationObserver(() => {
|
|
427
610
|
mutationCount++;
|
|
428
611
|
});
|
|
429
612
|
observer.observe(container, { childList: true, subtree: true });
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
observer.disconnect();
|
|
436
|
-
done();
|
|
437
|
-
}, 100);
|
|
438
|
-
}, 0);
|
|
613
|
+
|
|
614
|
+
state.content = '<span>Updated</span>';
|
|
615
|
+
await new Promise(r => setTimeout(r, 100));
|
|
616
|
+
expect(mutationCount).toBeLessThan(10); // Should not cause excessive mutations
|
|
617
|
+
observer.disconnect();
|
|
439
618
|
});
|
|
440
619
|
});
|
|
441
620
|
});
|
|
621
|
+
|
|
622
|
+
describe(':style Directive', () => {
|
|
623
|
+
let container;
|
|
624
|
+
|
|
625
|
+
beforeEach(() => {
|
|
626
|
+
container = document.createElement('div');
|
|
627
|
+
container.setAttribute('data-helium', '');
|
|
628
|
+
document.body.appendChild(container);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
afterEach(() => {
|
|
632
|
+
window.heliumTeardown?.();
|
|
633
|
+
document.body.innerHTML = '';
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('should apply style object with camelCase properties', async () => {
|
|
637
|
+
container.innerHTML = '<div :style="{ backgroundColor: color }"></div>';
|
|
638
|
+
await helium({ color: 'blue' });
|
|
639
|
+
const div = container.querySelector('div');
|
|
640
|
+
expect(div.style.backgroundColor).toBe('blue');
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('should apply style object with kebab-case properties', async () => {
|
|
644
|
+
container.innerHTML = '<div :style="{ \'background-color\': color }"></div>';
|
|
645
|
+
await helium({ color: 'red' });
|
|
646
|
+
const div = container.querySelector('div');
|
|
647
|
+
expect(div.style.backgroundColor).toBe('red');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('should apply multiple style properties', async () => {
|
|
651
|
+
container.innerHTML = '<div :style="{ backgroundColor: bg, fontSize: size }"></div>';
|
|
652
|
+
await helium({ bg: 'green', size: '20px' });
|
|
653
|
+
const div = container.querySelector('div');
|
|
654
|
+
expect(div.style.backgroundColor).toBe('green');
|
|
655
|
+
expect(div.style.fontSize).toBe('20px');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('should update style when state changes', async () => {
|
|
659
|
+
container.innerHTML = '<div :style="{ backgroundColor: color }"></div>';
|
|
660
|
+
const state = await helium({ color: 'blue' });
|
|
661
|
+
const div = container.querySelector('div');
|
|
662
|
+
expect(div.style.backgroundColor).toBe('blue');
|
|
663
|
+
|
|
664
|
+
state.color = 'red';
|
|
665
|
+
await new Promise(r => setTimeout(r, 0));
|
|
666
|
+
expect(div.style.backgroundColor).toBe('red');
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('should filter out null and false values', async () => {
|
|
670
|
+
container.innerHTML = '<div :style="{ backgroundColor: bg, color: textColor }"></div>';
|
|
671
|
+
await helium({ bg: 'blue', textColor: null });
|
|
672
|
+
const div = container.querySelector('div');
|
|
673
|
+
expect(div.style.backgroundColor).toBe('blue');
|
|
674
|
+
expect(div.style.color).toBe('');
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('should handle width with percentage', async () => {
|
|
678
|
+
container.innerHTML = '<div :style="{ width: width + \'%\' }"></div>';
|
|
679
|
+
await helium({ width: 50 });
|
|
680
|
+
const div = container.querySelector('div');
|
|
681
|
+
expect(div.style.width).toBe('50%');
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
describe('@calculate', () => {
|
|
686
|
+
let container;
|
|
687
|
+
|
|
688
|
+
beforeEach(() => {
|
|
689
|
+
container = document.createElement('div');
|
|
690
|
+
container.setAttribute('data-helium', '');
|
|
691
|
+
document.body.appendChild(container);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
afterEach(() => {
|
|
695
|
+
heliumTeardown();
|
|
696
|
+
document.body.innerHTML = '';
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('should compute derived values from state', async () => {
|
|
700
|
+
container.innerHTML = `
|
|
701
|
+
<div @data="{ firstname: 'John', lastname: 'Doe' }">
|
|
702
|
+
<span @calculate:fullname="firstname + ' ' + lastname"></span>
|
|
703
|
+
<span id="result" @text="fullname"></span>
|
|
704
|
+
</div>
|
|
705
|
+
`;
|
|
706
|
+
await helium();
|
|
707
|
+
expect(container.querySelector('#result').textContent).toBe('John Doe');
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('should update when dependencies change', async () => {
|
|
711
|
+
container.innerHTML = `
|
|
712
|
+
<div @data="{ x: 5, y: 3 }">
|
|
713
|
+
<span @calculate:sum="x + y"></span>
|
|
714
|
+
<span id="result" @text="sum"></span>
|
|
715
|
+
<button @click="x = 10">Update</button>
|
|
716
|
+
</div>
|
|
717
|
+
`;
|
|
718
|
+
const state = await helium();
|
|
719
|
+
expect(container.querySelector('#result').textContent).toBe('8');
|
|
720
|
+
|
|
721
|
+
container.querySelector('button').click();
|
|
722
|
+
await new Promise(r => setTimeout(r, 10));
|
|
723
|
+
expect(container.querySelector('#result').textContent).toBe('13');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('should update @text with fallback expression when @calculate value changes', async () => {
|
|
727
|
+
container.innerHTML = `
|
|
728
|
+
<div @data="{ firstname: '', lastname: '' }">
|
|
729
|
+
<input id="first" @bind="firstname">
|
|
730
|
+
<input id="last" @bind="lastname">
|
|
731
|
+
<span @calculate:fullname="firstname && lastname ? firstname + ' ' + lastname : firstname || lastname || ''"></span>
|
|
732
|
+
<span id="result" @text="fullname || 'stranger'"></span>
|
|
733
|
+
</div>
|
|
734
|
+
`;
|
|
735
|
+
const state = await helium();
|
|
736
|
+
|
|
737
|
+
// Initially should show 'stranger' since fullname is ''
|
|
738
|
+
expect(container.querySelector('#result').textContent).toBe('stranger');
|
|
739
|
+
|
|
740
|
+
// Update firstname
|
|
741
|
+
const firstInput = container.querySelector('#first');
|
|
742
|
+
firstInput.value = 'John';
|
|
743
|
+
firstInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
744
|
+
await new Promise(r => setTimeout(r, 10));
|
|
745
|
+
|
|
746
|
+
// Should now show 'John' (since lastname is still empty, fullname = 'John')
|
|
747
|
+
expect(container.querySelector('#result').textContent).toBe('John');
|
|
748
|
+
|
|
749
|
+
// Update lastname
|
|
750
|
+
const lastInput = container.querySelector('#last');
|
|
751
|
+
lastInput.value = 'Doe';
|
|
752
|
+
lastInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
753
|
+
await new Promise(r => setTimeout(r, 10));
|
|
754
|
+
|
|
755
|
+
// Should now show 'John Doe'
|
|
756
|
+
expect(container.querySelector('#result').textContent).toBe('John Doe');
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('should handle password strength pattern with nested property access', async () => {
|
|
760
|
+
container.innerHTML = `
|
|
761
|
+
<div @data="{ password: '' }">
|
|
762
|
+
<input id="pw" type="password" @bind="password">
|
|
763
|
+
<span @calculate:strength="password.length < 4 ? 'Weak' : password.length < 8 ? 'Medium' : 'Strong'"></span>
|
|
764
|
+
<span id="result" @text="strength"></span>
|
|
765
|
+
</div>
|
|
766
|
+
`;
|
|
767
|
+
await helium();
|
|
768
|
+
|
|
769
|
+
// Initially should show 'Weak' (empty password)
|
|
770
|
+
expect(container.querySelector('#result').textContent).toBe('Weak');
|
|
771
|
+
|
|
772
|
+
// Type 'abc' (3 chars) - still Weak
|
|
773
|
+
const input = container.querySelector('#pw');
|
|
774
|
+
input.value = 'abc';
|
|
775
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
776
|
+
await new Promise(r => setTimeout(r, 10));
|
|
777
|
+
expect(container.querySelector('#result').textContent).toBe('Weak');
|
|
778
|
+
|
|
779
|
+
// Type 'abcdef' (6 chars) - Medium
|
|
780
|
+
input.value = 'abcdef';
|
|
781
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
782
|
+
await new Promise(r => setTimeout(r, 10));
|
|
783
|
+
expect(container.querySelector('#result').textContent).toBe('Medium');
|
|
784
|
+
|
|
785
|
+
// Type 'abcdefgh' (8 chars) - Strong
|
|
786
|
+
input.value = 'abcdefgh';
|
|
787
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
788
|
+
await new Promise(r => setTimeout(r, 10));
|
|
789
|
+
expect(container.querySelector('#result').textContent).toBe('Strong');
|
|
790
|
+
});
|
|
791
|
+
});
|