a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { mount } from "$core/binder";
2import { signal } from "$core/signal";
3import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
5describe("input modifiers", () => {
6 beforeEach(() => {
7 vi.useFakeTimers();
8 });
9
10 afterEach(() => {
11 vi.restoreAllMocks();
12 });
13
14 describe("data-volt-model modifiers", () => {
15 describe(".number modifier", () => {
16 it("coerces string values to numbers", () => {
17 const input = document.createElement("input");
18 input.type = "text";
19 input.dataset.voltModelNumber = "count";
20
21 const count = signal(0);
22 mount(input, { count });
23
24 input.value = "42";
25 input.dispatchEvent(new Event("input"));
26
27 expect(count.get()).toBe(42);
28 });
29
30 it("handles empty strings as NaN", () => {
31 const input = document.createElement("input");
32 input.type = "text";
33 input.dataset.voltModelNumber = "value";
34
35 const value = signal(0);
36 mount(input, { value });
37
38 input.value = "";
39 input.dispatchEvent(new Event("input"));
40
41 expect(Number.isNaN(value.get())).toBe(true);
42 });
43
44 it("handles decimal numbers", () => {
45 const input = document.createElement("input");
46 input.type = "text";
47 input.dataset.voltModelNumber = "price";
48
49 const price = signal(0);
50 mount(input, { price });
51
52 input.value = "19.99";
53 input.dispatchEvent(new Event("input"));
54
55 expect(price.get()).toBe(19.99);
56 });
57 });
58
59 describe(".trim modifier", () => {
60 it("trims whitespace from string values", () => {
61 const input = document.createElement("input");
62 input.type = "text";
63 input.dataset.voltModelTrim = "name";
64
65 const name = signal("");
66 mount(input, { name });
67
68 input.value = " John Doe ";
69 input.dispatchEvent(new Event("input"));
70
71 expect(name.get()).toBe("John Doe");
72 });
73
74 it("handles strings with only whitespace", () => {
75 const input = document.createElement("input");
76 input.type = "text";
77 input.dataset.voltModelTrim = "value";
78
79 const value = signal("");
80 mount(input, { value });
81
82 input.value = " ";
83 input.dispatchEvent(new Event("input"));
84
85 expect(value.get()).toBe("");
86 });
87 });
88
89 describe(".lazy modifier", () => {
90 it("syncs on change event instead of input", () => {
91 const input = document.createElement("input");
92 input.type = "text";
93 input.dataset.voltModelLazy = "value";
94
95 const value = signal("");
96 mount(input, { value });
97
98 input.value = "test";
99 input.dispatchEvent(new Event("input"));
100 expect(value.get()).toBe("");
101
102 input.dispatchEvent(new Event("change"));
103 expect(value.get()).toBe("test");
104 });
105
106 it("works with checkboxes", () => {
107 const input = document.createElement("input");
108 input.type = "checkbox";
109 input.dataset.voltModelLazy = "checked";
110
111 const checked = signal(false);
112 mount(input, { checked });
113
114 input.checked = true;
115 input.dispatchEvent(new Event("change"));
116
117 expect(checked.get()).toBe(true);
118 });
119 });
120
121 describe(".debounce modifier", () => {
122 it("debounces signal updates with default delay (300ms)", () => {
123 const input = document.createElement("input");
124 input.type = "text";
125 input.dataset.voltModelDebounce = "search";
126
127 const search = signal("");
128 mount(input, { search });
129
130 input.value = "hello";
131 input.dispatchEvent(new Event("input"));
132
133 expect(search.get()).toBe("");
134
135 vi.advanceTimersByTime(299);
136 expect(search.get()).toBe("");
137
138 vi.advanceTimersByTime(1);
139 expect(search.get()).toBe("hello");
140 });
141
142 it("supports custom debounce delay", () => {
143 const input = document.createElement("input");
144 input.type = "text";
145 input.dataset.voltModelDebounce500 = "search";
146
147 const search = signal("");
148 mount(input, { search });
149
150 input.value = "test";
151 input.dispatchEvent(new Event("input"));
152
153 vi.advanceTimersByTime(499);
154 expect(search.get()).toBe("");
155
156 vi.advanceTimersByTime(1);
157 expect(search.get()).toBe("test");
158 });
159
160 it("resets timer on subsequent inputs", () => {
161 const input = document.createElement("input");
162 input.type = "text";
163 input.dataset.voltModelDebounce100 = "value";
164
165 const value = signal("");
166 mount(input, { value });
167
168 input.value = "a";
169 input.dispatchEvent(new Event("input"));
170 vi.advanceTimersByTime(50);
171
172 input.value = "ab";
173 input.dispatchEvent(new Event("input"));
174 vi.advanceTimersByTime(50);
175
176 expect(value.get()).toBe("");
177
178 vi.advanceTimersByTime(50);
179 expect(value.get()).toBe("ab");
180 });
181
182 it("cancels pending updates on cleanup", () => {
183 const input = document.createElement("input");
184 input.type = "text";
185 input.dataset.voltModelDebounce100 = "value";
186
187 const value = signal("");
188 const cleanup = mount(input, { value });
189
190 input.value = "test";
191 input.dispatchEvent(new Event("input"));
192
193 vi.advanceTimersByTime(50);
194 cleanup();
195
196 vi.advanceTimersByTime(100);
197 expect(value.get()).toBe("");
198 });
199 });
200
201 describe("modifier combinations", () => {
202 it("combines .number and .trim", () => {
203 const input = document.createElement("input");
204 input.type = "text";
205 input.dataset.voltModelNumberTrim = "value";
206
207 const value = signal(0);
208 mount(input, { value });
209
210 input.value = " 42 ";
211 input.dispatchEvent(new Event("input"));
212
213 expect(value.get()).toBe(42);
214 });
215
216 it("combines .trim and .lazy", () => {
217 const input = document.createElement("input");
218 input.type = "text";
219 input.dataset.voltModelTrimLazy = "value";
220
221 const value = signal("");
222 mount(input, { value });
223
224 input.value = " test ";
225 input.dispatchEvent(new Event("input"));
226 expect(value.get()).toBe("");
227
228 input.dispatchEvent(new Event("change"));
229 expect(value.get()).toBe("test");
230 });
231
232 it("combines .number and .debounce", () => {
233 const input = document.createElement("input");
234 input.type = "text";
235 input.dataset.voltModelNumberDebounce100 = "value";
236
237 const value = signal(0);
238 mount(input, { value });
239
240 input.value = "123";
241 input.dispatchEvent(new Event("input"));
242
243 expect(value.get()).toBe(0);
244
245 vi.advanceTimersByTime(100);
246 expect(value.get()).toBe(123);
247 });
248
249 it("combines .trim, .number, and .debounce", () => {
250 const input = document.createElement("input");
251 input.type = "text";
252 input.dataset.voltModelTrimNumberDebounce100 = "value";
253
254 const value = signal(0);
255 mount(input, { value });
256
257 input.value = " 456 ";
258 input.dispatchEvent(new Event("input"));
259
260 expect(value.get()).toBe(0);
261
262 vi.advanceTimersByTime(100);
263 expect(value.get()).toBe(456);
264 });
265 });
266 });
267
268 describe("data-volt-bind modifiers", () => {
269 describe(".number modifier", () => {
270 it("coerces attribute values to numbers", () => {
271 const div = document.createElement("div");
272 div.dataset.voltBindValueNumber = "count";
273
274 const count = signal(42);
275 mount(div, { count });
276
277 expect(div.getAttribute("value")).toBe("42");
278
279 count.set(100);
280 expect(div.getAttribute("value")).toBe("100");
281 });
282
283 it("handles string expressions with .number", () => {
284 const div = document.createElement("div");
285 div.dataset.voltBindPriceNumber = "' 123 '";
286
287 mount(div, {});
288
289 expect(div.getAttribute("price")).toBe("123");
290 });
291 });
292
293 describe(".trim modifier", () => {
294 it("trims attribute values", () => {
295 const div = document.createElement("div");
296 div.dataset.voltBindTitleTrim = "title";
297
298 const title = signal(" Hello World ");
299 mount(div, { title });
300
301 expect(div.getAttribute("title")).toBe("Hello World");
302 });
303
304 it("handles expressions that evaluate to strings", () => {
305 const div = document.createElement("div");
306 div.dataset.voltBindNameTrim = "' test '";
307
308 mount(div, {});
309
310 expect(div.getAttribute("name")).toBe("test");
311 });
312 });
313
314 describe("modifier combinations", () => {
315 it("combines .trim and .number", () => {
316 const div = document.createElement("div");
317 div.dataset.voltBindValueTrimNumber = "value";
318
319 const value = signal(" 42 ");
320 mount(div, { value });
321
322 expect(div.getAttribute("value")).toBe("42");
323 });
324 });
325 });
326
327 describe("signal synchronization", () => {
328 it("updates input value when signal changes", () => {
329 const input = document.createElement("input");
330 input.type = "text";
331 input.dataset.voltModelNumber = "count";
332
333 const count = signal(10);
334 mount(input, { count });
335
336 expect(input.value).toBe("10");
337
338 count.set(20);
339 expect(input.value).toBe("20");
340 });
341
342 it("maintains two-way binding with .number modifier", () => {
343 const input = document.createElement("input");
344 input.type = "text";
345 input.dataset.voltModelNumber = "value";
346
347 const value = signal(5);
348 mount(input, { value });
349
350 expect(input.value).toBe("5");
351
352 value.set(10);
353 expect(input.value).toBe("10");
354
355 input.value = "15";
356 input.dispatchEvent(new Event("input"));
357 expect(value.get()).toBe(15);
358 });
359 });
360});