a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 10 kB view raw
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});