a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 410 lines 12 kB view raw
1import { mount } from "$core/binder"; 2import { signal } from "$core/signal"; 3import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 4 5describe("event modifiers", () => { 6 beforeEach(() => { 7 vi.useFakeTimers(); 8 }); 9 10 afterEach(() => { 11 vi.restoreAllMocks(); 12 }); 13 14 describe(".prevent modifier", () => { 15 it("calls preventDefault on the event", () => { 16 const form = document.createElement("form"); 17 form.dataset.voltOnSubmit = "handleSubmit"; 18 form.dataset.voltOnSubmitPrevent = ""; 19 20 const handleSubmit = vi.fn(); 21 mount(form, { handleSubmit }); 22 23 const submitEvent = new Event("submit", { cancelable: true }); 24 form.dispatchEvent(submitEvent); 25 26 expect(submitEvent.defaultPrevented).toBe(true); 27 expect(handleSubmit).toHaveBeenCalled(); 28 }); 29 30 it("works with inline expressions", () => { 31 const button = document.createElement("button"); 32 button.dataset.voltOnClickPrevent = "count.set(count.get() + 1)"; 33 34 const count = signal(0); 35 mount(button, { count }); 36 37 const clickEvent = new MouseEvent("click", { cancelable: true }); 38 button.dispatchEvent(clickEvent); 39 40 expect(clickEvent.defaultPrevented).toBe(true); 41 expect(count.get()).toBe(1); 42 }); 43 }); 44 45 describe(".stop modifier", () => { 46 it("calls stopPropagation on the event", () => { 47 const div = document.createElement("div"); 48 const button = document.createElement("button"); 49 button.dataset.voltOnClickStop = "handleClick"; 50 div.append(button); 51 52 const handleClick = vi.fn(); 53 const handleDivClick = vi.fn(); 54 55 mount(button, { handleClick }); 56 div.addEventListener("click", handleDivClick); 57 58 button.click(); 59 60 expect(handleClick).toHaveBeenCalled(); 61 expect(handleDivClick).not.toHaveBeenCalled(); 62 }); 63 }); 64 65 describe(".self modifier", () => { 66 it("only triggers when event.target is the bound element", () => { 67 const div = document.createElement("div"); 68 const span = document.createElement("span"); 69 div.dataset.voltOnClickSelf = "handleClick"; 70 div.append(span); 71 72 const handleClick = vi.fn(); 73 mount(div, { handleClick }); 74 75 span.click(); 76 expect(handleClick).not.toHaveBeenCalled(); 77 78 div.click(); 79 expect(handleClick).toHaveBeenCalled(); 80 }); 81 }); 82 83 describe(".once modifier", () => { 84 it("only triggers the handler once", () => { 85 const button = document.createElement("button"); 86 button.dataset.voltOnClickOnce = "handleClick"; 87 88 const handleClick = vi.fn(); 89 mount(button, { handleClick }); 90 91 button.click(); 92 expect(handleClick).toHaveBeenCalledTimes(1); 93 94 button.click(); 95 expect(handleClick).toHaveBeenCalledTimes(1); 96 }); 97 }); 98 99 describe(".passive modifier", () => { 100 it("adds passive event listener", () => { 101 const div = document.createElement("div"); 102 div.dataset.voltOnScrollPassive = "handleScroll"; 103 104 const handleScroll = vi.fn(); 105 const addEventListenerSpy = vi.spyOn(div, "addEventListener"); 106 107 mount(div, { handleScroll }); 108 109 expect(addEventListenerSpy).toHaveBeenCalledWith("scroll", expect.any(Function), { passive: true }); 110 }); 111 }); 112 113 describe(".window modifier", () => { 114 it("attaches listener to window", () => { 115 const button = document.createElement("button"); 116 button.dataset.voltOnResizeWindow = "handleResize"; 117 118 const handleResize = vi.fn(); 119 const cleanup = mount(button, { handleResize }); 120 121 globalThis.dispatchEvent(new Event("resize")); 122 expect(handleResize).toHaveBeenCalled(); 123 124 cleanup(); 125 }); 126 127 it("still provides $el context", () => { 128 const button = document.createElement("button"); 129 button.id = "test-button"; 130 button.dataset.voltOnClickWindow = "elementId.set($el.id)"; 131 132 const elementId = signal(""); 133 const cleanup = mount(button, { elementId }); 134 135 globalThis.dispatchEvent(new MouseEvent("click")); 136 expect(elementId.get()).toBe("test-button"); 137 138 cleanup(); 139 }); 140 }); 141 142 describe(".document modifier", () => { 143 it("attaches listener to document", () => { 144 const button = document.createElement("button"); 145 button.dataset.voltOnClickDocument = "handleClick"; 146 147 const handleClick = vi.fn(); 148 const cleanup = mount(button, { handleClick }); 149 150 document.dispatchEvent(new MouseEvent("click")); 151 expect(handleClick).toHaveBeenCalled(); 152 153 cleanup(); 154 }); 155 156 it("still provides $el context", () => { 157 const button = document.createElement("button"); 158 button.id = "doc-button"; 159 button.dataset.voltOnKeydownDocument = "elementId.set($el.id)"; 160 161 const elementId = signal(""); 162 const cleanup = mount(button, { elementId }); 163 164 document.dispatchEvent(new KeyboardEvent("keydown")); 165 expect(elementId.get()).toBe("doc-button"); 166 167 cleanup(); 168 }); 169 }); 170 171 describe(".debounce modifier", () => { 172 it("debounces handler with default delay (300ms)", () => { 173 const input = document.createElement("input"); 174 input.dataset.voltOnInputDebounce = "handleInput"; 175 176 const handleInput = vi.fn(); 177 mount(input, { handleInput }); 178 179 input.dispatchEvent(new Event("input")); 180 expect(handleInput).not.toHaveBeenCalled(); 181 182 vi.advanceTimersByTime(299); 183 expect(handleInput).not.toHaveBeenCalled(); 184 185 vi.advanceTimersByTime(1); 186 expect(handleInput).toHaveBeenCalledTimes(1); 187 }); 188 189 it("supports custom debounce delay", () => { 190 const input = document.createElement("input"); 191 input.dataset.voltOnInputDebounce500 = "handleInput"; 192 193 const handleInput = vi.fn(); 194 mount(input, { handleInput }); 195 196 input.dispatchEvent(new Event("input")); 197 vi.advanceTimersByTime(499); 198 expect(handleInput).not.toHaveBeenCalled(); 199 200 vi.advanceTimersByTime(1); 201 expect(handleInput).toHaveBeenCalledTimes(1); 202 }); 203 204 it("resets timer on subsequent events", () => { 205 const input = document.createElement("input"); 206 input.dataset.voltOnInputDebounce100 = "handleInput"; 207 208 const handleInput = vi.fn(); 209 mount(input, { handleInput }); 210 211 input.dispatchEvent(new Event("input")); 212 vi.advanceTimersByTime(50); 213 214 input.dispatchEvent(new Event("input")); 215 vi.advanceTimersByTime(50); 216 expect(handleInput).not.toHaveBeenCalled(); 217 218 vi.advanceTimersByTime(50); 219 expect(handleInput).toHaveBeenCalledTimes(1); 220 }); 221 222 it("cancels pending debounced calls on cleanup", () => { 223 const input = document.createElement("input"); 224 input.dataset.voltOnInputDebounce100 = "handleInput"; 225 226 const handleInput = vi.fn(); 227 const cleanup = mount(input, { handleInput }); 228 229 input.dispatchEvent(new Event("input")); 230 vi.advanceTimersByTime(50); 231 232 cleanup(); 233 234 vi.advanceTimersByTime(100); 235 expect(handleInput).not.toHaveBeenCalled(); 236 }); 237 }); 238 239 describe(".throttle modifier", () => { 240 it("throttles handler with default delay (300ms)", () => { 241 const button = document.createElement("button"); 242 button.dataset.voltOnClickThrottle = "handleClick"; 243 244 const handleClick = vi.fn(); 245 mount(button, { handleClick }); 246 247 button.click(); 248 expect(handleClick).toHaveBeenCalledTimes(1); 249 250 button.click(); 251 expect(handleClick).toHaveBeenCalledTimes(1); 252 253 vi.advanceTimersByTime(300); 254 expect(handleClick).toHaveBeenCalledTimes(2); 255 }); 256 257 it("supports custom throttle delay", () => { 258 const button = document.createElement("button"); 259 button.dataset.voltOnClickThrottle100 = "handleClick"; 260 261 const handleClick = vi.fn(); 262 mount(button, { handleClick }); 263 264 button.click(); 265 expect(handleClick).toHaveBeenCalledTimes(1); 266 267 button.click(); 268 vi.advanceTimersByTime(100); 269 expect(handleClick).toHaveBeenCalledTimes(2); 270 }); 271 272 it("executes immediately on first call", () => { 273 const button = document.createElement("button"); 274 button.dataset.voltOnClickThrottle100 = "handleClick"; 275 276 const handleClick = vi.fn(); 277 mount(button, { handleClick }); 278 279 button.click(); 280 expect(handleClick).toHaveBeenCalledTimes(1); 281 }); 282 283 it("cancels pending throttled calls on cleanup", () => { 284 const button = document.createElement("button"); 285 button.dataset.voltOnClickThrottle100 = "handleClick"; 286 287 const handleClick = vi.fn(); 288 const cleanup = mount(button, { handleClick }); 289 290 button.click(); 291 expect(handleClick).toHaveBeenCalledTimes(1); 292 293 button.click(); 294 cleanup(); 295 296 vi.advanceTimersByTime(100); 297 expect(handleClick).toHaveBeenCalledTimes(1); 298 }); 299 }); 300 301 describe("modifier combinations", () => { 302 it("combines .prevent and .stop", () => { 303 const form = document.createElement("form"); 304 const button = document.createElement("button"); 305 button.type = "submit"; 306 button.dataset.voltOnClickPreventStop = "handleClick"; 307 form.append(button); 308 309 const handleClick = vi.fn(); 310 const handleFormSubmit = vi.fn(); 311 312 mount(button, { handleClick }); 313 form.addEventListener("click", handleFormSubmit); 314 315 const clickEvent = new MouseEvent("click", { cancelable: true, bubbles: true }); 316 button.dispatchEvent(clickEvent); 317 318 expect(clickEvent.defaultPrevented).toBe(true); 319 expect(handleClick).toHaveBeenCalled(); 320 expect(handleFormSubmit).not.toHaveBeenCalled(); 321 }); 322 323 it("combines .self and .prevent", () => { 324 const div = document.createElement("div"); 325 const span = document.createElement("span"); 326 div.dataset.voltOnClickSelfPrevent = "handleClick"; 327 div.append(span); 328 329 const handleClick = vi.fn(); 330 mount(div, { handleClick }); 331 332 const spanEvent = new MouseEvent("click", { cancelable: true, bubbles: true }); 333 span.dispatchEvent(spanEvent); 334 expect(handleClick).not.toHaveBeenCalled(); 335 expect(spanEvent.defaultPrevented).toBe(false); 336 337 const divEvent = new MouseEvent("click", { cancelable: true }); 338 div.dispatchEvent(divEvent); 339 expect(handleClick).toHaveBeenCalled(); 340 expect(divEvent.defaultPrevented).toBe(true); 341 }); 342 343 it("combines .debounce with .prevent", () => { 344 const form = document.createElement("form"); 345 form.dataset.voltOnSubmitDebounce100Prevent = "handleSubmit"; 346 347 const handleSubmit = vi.fn(); 348 mount(form, { handleSubmit }); 349 350 const submitEvent = new Event("submit", { cancelable: true }); 351 form.dispatchEvent(submitEvent); 352 353 expect(submitEvent.defaultPrevented).toBe(true); 354 expect(handleSubmit).not.toHaveBeenCalled(); 355 356 vi.advanceTimersByTime(100); 357 expect(handleSubmit).toHaveBeenCalledTimes(1); 358 }); 359 }); 360 361 describe("cleanup", () => { 362 it("removes event listeners on unmount", () => { 363 const button = document.createElement("button"); 364 button.dataset.voltOnClick = "handleClick"; 365 366 const handleClick = vi.fn(); 367 const cleanup = mount(button, { handleClick }); 368 369 button.click(); 370 expect(handleClick).toHaveBeenCalledTimes(1); 371 372 cleanup(); 373 374 button.click(); 375 expect(handleClick).toHaveBeenCalledTimes(1); 376 }); 377 378 it("removes window event listeners on unmount", () => { 379 const button = document.createElement("button"); 380 button.dataset.voltOnResizeWindow = "handleResize"; 381 382 const handleResize = vi.fn(); 383 const cleanup = mount(button, { handleResize }); 384 385 globalThis.dispatchEvent(new Event("resize")); 386 expect(handleResize).toHaveBeenCalledTimes(1); 387 388 cleanup(); 389 390 globalThis.dispatchEvent(new Event("resize")); 391 expect(handleResize).toHaveBeenCalledTimes(1); 392 }); 393 394 it("removes document event listeners on unmount", () => { 395 const button = document.createElement("button"); 396 button.dataset.voltOnClickDocument = "handleClick"; 397 398 const handleClick = vi.fn(); 399 const cleanup = mount(button, { handleClick }); 400 401 document.dispatchEvent(new MouseEvent("click")); 402 expect(handleClick).toHaveBeenCalledTimes(1); 403 404 cleanup(); 405 406 document.dispatchEvent(new MouseEvent("click")); 407 expect(handleClick).toHaveBeenCalledTimes(1); 408 }); 409 }); 410});