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 565 lines 16 kB view raw
1import { shiftPlugin } from "$plugins/shift"; 2import { surgePlugin } from "$plugins/surge"; 3import type { TransitionPreset } from "$types/volt"; 4import { mount, registerPlugin, registerTransition, signal } from "$volt"; 5import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 6 7describe("integration: transitions", () => { 8 beforeEach(() => { 9 registerPlugin("surge", surgePlugin); 10 registerPlugin("shift", shiftPlugin); 11 globalThis.matchMedia = vi.fn().mockReturnValue({ matches: false }); 12 }); 13 14 afterEach(() => { 15 vi.restoreAllMocks(); 16 }); 17 18 describe("Surge with data-volt-if", () => { 19 it("should animate element in when condition becomes true", async () => { 20 vi.useFakeTimers(); 21 22 const container = document.createElement("div"); 23 container.innerHTML = `<div data-volt-if="show" data-volt-surge="fade">Content</div>`; 24 25 const show = signal(false); 26 mount(container, { show }); 27 28 const comment = [...container.childNodes].find((node) => node.nodeType === 8); 29 expect(comment).toBeDefined(); 30 31 show.set(true); 32 33 await vi.advanceTimersByTimeAsync(400); 34 35 const element = container.querySelector("div"); 36 expect(element).toBeDefined(); 37 if (element) { 38 expect(element.textContent).toContain("Content"); 39 } 40 41 vi.useRealTimers(); 42 }); 43 44 it("should animate element out when condition becomes false", async () => { 45 vi.useFakeTimers(); 46 47 const container = document.createElement("div"); 48 container.innerHTML = ` 49 <div data-volt-if="show" data-volt-surge="fade"> 50 Content 51 </div> 52 `; 53 54 const show = signal(true); 55 mount(container, { show }); 56 57 let element = container.querySelector("div"); 58 expect(element).toBeDefined(); 59 60 show.set(false); 61 62 await vi.advanceTimersByTimeAsync(400); 63 64 element = container.querySelector("div"); 65 expect(element).toBeNull(); 66 67 vi.useRealTimers(); 68 }); 69 70 it("should support custom enter/leave transitions", async () => { 71 vi.useFakeTimers(); 72 73 const container = document.createElement("div"); 74 container.innerHTML = ` 75 <div 76 data-volt-if="show" 77 data-volt-surge:enter="slide-down" 78 data-volt-surge:leave="fade"> 79 Content 80 </div> 81 `; 82 83 const show = signal(false); 84 mount(container, { show }); 85 86 show.set(true); 87 await vi.advanceTimersByTimeAsync(400); 88 89 let element = container.querySelector("div"); 90 expect(element).toBeDefined(); 91 92 show.set(false); 93 await vi.advanceTimersByTimeAsync(400); 94 95 element = container.querySelector("div"); 96 expect(element).toBeNull(); 97 98 vi.useRealTimers(); 99 }); 100 101 it("should work with if/else pattern", async () => { 102 vi.useFakeTimers(); 103 104 const container = document.createElement("div"); 105 container.innerHTML = ` 106 <div data-volt-if="show" data-volt-surge="fade"> 107 Shown 108 </div> 109 <div data-volt-else data-volt-surge="fade"> 110 Hidden 111 </div> 112 `; 113 114 const show = signal(true); 115 mount(container, { show }); 116 117 await vi.advanceTimersByTimeAsync(400); 118 119 let shownEl = [...container.querySelectorAll("div")].find((el) => el.textContent?.includes("Shown")); 120 let hiddenEl = [...container.querySelectorAll("div")].find((el) => el.textContent?.includes("Hidden")); 121 122 expect(shownEl).toBeDefined(); 123 expect(hiddenEl).toBeUndefined(); 124 125 show.set(false); 126 await vi.advanceTimersByTimeAsync(400); 127 128 shownEl = [...container.querySelectorAll("div")].find((el) => el.textContent?.includes("Shown")); 129 hiddenEl = [...container.querySelectorAll("div")].find((el) => el.textContent?.includes("Hidden")); 130 131 expect(shownEl).toBeUndefined(); 132 expect(hiddenEl).toBeDefined(); 133 134 vi.useRealTimers(); 135 }); 136 137 it("should support duration and delay modifiers", async () => { 138 vi.useFakeTimers(); 139 140 const container = document.createElement("div"); 141 container.innerHTML = ` 142 <div data-volt-if="show" data-volt-surge="fade.500.100"> 143 Content 144 </div> 145 `; 146 147 const show = signal(false); 148 mount(container, { show }); 149 150 show.set(true); 151 await vi.advanceTimersByTimeAsync(650); 152 153 const element = container.querySelector("div"); 154 expect(element).toBeDefined(); 155 156 vi.useRealTimers(); 157 }); 158 }); 159 160 describe("Surge with data-volt-show", () => { 161 it("should toggle display property with transition", async () => { 162 vi.useFakeTimers(); 163 164 const container = document.createElement("div"); 165 const testEl = document.createElement("div"); 166 testEl.dataset.voltShow = "visible"; 167 testEl.dataset.voltSurge = "fade"; 168 testEl.textContent = "Content"; 169 container.append(testEl); 170 171 const visible = signal(true); 172 173 const element = testEl; 174 175 mount(container, { visible }); 176 177 expect(element.style.display).not.toBe("none"); 178 179 visible.set(false); 180 await vi.advanceTimersByTimeAsync(400); 181 182 expect(element.style.display).toBe("none"); 183 184 visible.set(true); 185 await vi.advanceTimersByTimeAsync(400); 186 187 expect(element.style.display).not.toBe("none"); 188 189 vi.useRealTimers(); 190 }); 191 192 it("should not start overlapping transitions", async () => { 193 vi.useFakeTimers(); 194 195 const container = document.createElement("div"); 196 container.innerHTML = ` 197 <div data-volt-show="visible" data-volt-surge="fade"> 198 Content 199 </div> 200 `; 201 202 const visible = signal(true); 203 mount(container, { visible }); 204 205 const element = container.querySelector("div") as HTMLElement; 206 207 visible.set(false); 208 visible.set(true); 209 visible.set(false); 210 211 await vi.advanceTimersByTimeAsync(50); 212 213 expect(element).toBeDefined(); 214 215 vi.useRealTimers(); 216 }); 217 }); 218 219 describe("Surge signal-triggered mode", () => { 220 it("should watch signal and apply transitions", async () => { 221 vi.useFakeTimers(); 222 223 const container = document.createElement("div"); 224 const testEl = document.createElement("div"); 225 testEl.dataset.voltSurge = "show:fade"; 226 testEl.textContent = "Content"; 227 container.append(testEl); 228 229 const show = signal(false); 230 231 const element = testEl; 232 233 mount(container, { show }); 234 235 expect(element.style.display).toBe("none"); 236 237 show.set(true); 238 await vi.advanceTimersByTimeAsync(400); 239 240 expect(element.style.display).not.toBe("none"); 241 242 show.set(false); 243 await vi.advanceTimersByTimeAsync(400); 244 245 expect(element.style.display).toBe("none"); 246 247 vi.useRealTimers(); 248 }); 249 250 it("should cleanup subscription on unmount", async () => { 251 const container = document.createElement("div"); 252 const testEl = document.createElement("div"); 253 testEl.dataset.voltSurge = "show:fade"; 254 testEl.textContent = "Content"; 255 container.append(testEl); 256 257 const show = signal(false); 258 const element = testEl; 259 260 const cleanup = mount(container, { show }); 261 262 expect(element.style.display).toBe("none"); 263 264 cleanup(); 265 266 const initialDisplay = element.style.display; 267 show.set(true); 268 269 await new Promise((resolve) => { 270 setTimeout(resolve, 50); 271 }); 272 273 expect(element.style.display).toBe(initialDisplay); 274 }); 275 }); 276 277 describe("Shift animations", () => { 278 it("should apply animation on mount", async () => { 279 const container = document.createElement("div"); 280 const testEl = document.createElement("div"); 281 testEl.dataset.voltShift = "bounce"; 282 testEl.textContent = "Content"; 283 container.append(testEl); 284 285 const element = testEl; 286 287 mount(container, {}); 288 289 // Wait for requestAnimationFrame to apply the animation 290 await vi.waitFor(() => { 291 expect(element.dataset.voltShiftRuns).toBe("1"); 292 expect(element.style.animationName).toMatch(/^volt-shift-/); 293 }); 294 }); 295 296 it("should trigger animation based on signal", () => { 297 const container = document.createElement("div"); 298 container.innerHTML = ` 299 <button data-volt-shift="trigger:bounce"> 300 Click me 301 </button> 302 `; 303 304 const trigger = signal(false); 305 mount(container, { trigger }); 306 307 const button = container.querySelector("button") as HTMLElement; 308 expect(button.dataset.voltShiftRuns ?? "0").toBe("0"); 309 310 trigger.set(true); 311 expect(button.dataset.voltShiftRuns).toBe("1"); 312 }); 313 314 it("should support duration and iteration modifiers", async () => { 315 const container = document.createElement("div"); 316 const testEl = document.createElement("div"); 317 testEl.dataset.voltShift = "bounce.1000.3"; 318 testEl.textContent = "Content"; 319 container.append(testEl); 320 321 const element = testEl; 322 323 mount(container, {}); 324 325 // Wait for requestAnimationFrame to apply the animation 326 await vi.waitFor(() => { 327 expect(element.dataset.voltShiftRuns).toBe("1"); 328 expect(element.style.animationDuration).toBe("1000ms"); 329 expect(element.style.animationIterationCount).toBe("3"); 330 }); 331 }); 332 333 it("should cleanup signal subscription on unmount", () => { 334 const container = document.createElement("div"); 335 container.innerHTML = ` 336 <button data-volt-shift="trigger:bounce"> 337 Click me 338 </button> 339 `; 340 341 const trigger = signal(false); 342 const cleanup = mount(container, { trigger }); 343 344 cleanup(); 345 346 const button = container.querySelector("button") as HTMLElement; 347 trigger.set(true); 348 349 expect(button.dataset.voltShiftRuns ?? "0").toBe("0"); 350 }); 351 }); 352 353 describe("Custom presets", () => { 354 it("should use registered custom transition preset", async () => { 355 vi.useFakeTimers(); 356 357 const customPreset: TransitionPreset = { 358 enter: { 359 from: { opacity: 0, transform: "scale(0.5)" }, 360 to: { opacity: 1, transform: "scale(1)" }, 361 duration: 200, 362 easing: "ease-out", 363 }, 364 leave: { 365 from: { opacity: 1, transform: "scale(1)" }, 366 to: { opacity: 0, transform: "scale(0.5)" }, 367 duration: 200, 368 easing: "ease-in", 369 }, 370 }; 371 372 registerTransition("custom-scale", customPreset); 373 374 const container = document.createElement("div"); 375 container.innerHTML = ` 376 <div data-volt-if="show" data-volt-surge="custom-scale"> 377 Content 378 </div> 379 `; 380 381 const show = signal(false); 382 mount(container, { show }); 383 384 show.set(true); 385 await vi.advanceTimersByTimeAsync(300); 386 387 const element = container.querySelector("div"); 388 expect(element).toBeDefined(); 389 390 vi.useRealTimers(); 391 }); 392 }); 393 394 describe("Accessibility: prefers-reduced-motion", () => { 395 it("should skip animations when user prefers reduced motion", async () => { 396 globalThis.matchMedia = vi.fn().mockReturnValue({ matches: true }); 397 398 const container = document.createElement("div"); 399 container.innerHTML = ` 400 <div data-volt-if="show" data-volt-surge="fade"> 401 Content 402 </div> 403 `; 404 405 const show = signal(false); 406 mount(container, { show }); 407 408 show.set(true); 409 410 await new Promise((resolve) => { 411 setTimeout(resolve, 50); 412 }); 413 414 const element = container.querySelector("div"); 415 expect(element).toBeDefined(); 416 }); 417 }); 418 419 describe("View Transitions API", () => { 420 it("should use View Transitions API when available", async () => { 421 const mockStartViewTransition = vi.fn((callback: () => void | Promise<void>) => { 422 const result = callback(); 423 return { 424 finished: Promise.resolve(result).then(() => {}), 425 ready: Promise.resolve(), 426 updateCallbackDone: Promise.resolve(result).then(() => {}), 427 skipTransition: vi.fn(), 428 }; 429 }); 430 431 // @ts-expect-error - Adding View Transitions API mock 432 document.startViewTransition = mockStartViewTransition; 433 434 const { startViewTransition } = await import("$core/view-transitions"); 435 436 await startViewTransition(() => { 437 const el = document.createElement("div"); 438 el.textContent = "test"; 439 }); 440 441 expect(mockStartViewTransition).toHaveBeenCalled(); 442 443 // @ts-expect-error - Cleanup mock 444 delete document.startViewTransition; 445 }); 446 447 it("should fallback to CSS when View Transitions API not available", async () => { 448 // @ts-expect-error - Ensure View Transitions API is not available 449 delete document.startViewTransition; 450 451 vi.useFakeTimers(); 452 453 const container = document.createElement("div"); 454 container.innerHTML = ` 455 <div data-volt-if="show" data-volt-surge="fade"> 456 Content 457 </div> 458 `; 459 460 const show = signal(false); 461 mount(container, { show }); 462 463 show.set(true); 464 await vi.advanceTimersByTimeAsync(400); 465 466 const element = container.querySelector("div"); 467 expect(element).toBeDefined(); 468 469 vi.useRealTimers(); 470 }); 471 }); 472 473 describe("Memory leak prevention", () => { 474 it("should cleanup all transition-related subscriptions", async () => { 475 const container = document.createElement("div"); 476 container.innerHTML = ` 477 <div data-volt-if="show" data-volt-surge="fade"> 478 Content 1 479 </div> 480 <div data-volt-show="visible" data-volt-surge="slide-down"> 481 Content 2 482 </div> 483 <div data-volt-surge="trigger:scale"> 484 Content 3 485 </div> 486 <button data-volt-shift="animTrigger:bounce"> 487 Content 4 488 </button> 489 `; 490 491 const show = signal(false); 492 const visible = signal(true); 493 const trigger = signal(false); 494 const animTrigger = signal(false); 495 496 const cleanup = mount(container, { show, visible, trigger, animTrigger }); 497 498 cleanup(); 499 500 const initialHTML = container.innerHTML; 501 502 show.set(true); 503 visible.set(false); 504 trigger.set(true); 505 animTrigger.set(true); 506 507 await new Promise((resolve) => { 508 setTimeout(resolve, 50); 509 }); 510 511 expect(container.innerHTML).toBe(initialHTML); 512 }); 513 }); 514 515 describe("Complex integration scenarios", () => { 516 it("should handle multiple animated elements simultaneously", async () => { 517 vi.useFakeTimers(); 518 519 const container = document.createElement("div"); 520 container.innerHTML = ` 521 <div data-volt-if="show1" data-volt-surge="fade">Item 1</div> 522 <div data-volt-if="show2" data-volt-surge="slide-down">Item 2</div> 523 <div data-volt-if="show3" data-volt-surge="scale">Item 3</div> 524 `; 525 526 const show1 = signal(false); 527 const show2 = signal(false); 528 const show3 = signal(false); 529 530 mount(container, { show1, show2, show3 }); 531 532 show1.set(true); 533 show2.set(true); 534 show3.set(true); 535 536 await vi.advanceTimersByTimeAsync(400); 537 538 const elements = container.querySelectorAll("div"); 539 expect(elements.length).toBe(3); 540 541 vi.useRealTimers(); 542 }); 543 544 it("should combine surge and shift on same element", async () => { 545 const container = document.createElement("div"); 546 const testEl = document.createElement("div"); 547 testEl.dataset.voltShow = "visible"; 548 testEl.dataset.voltSurge = "fade"; 549 testEl.dataset.voltShift = "bounce"; 550 testEl.textContent = "Combined"; 551 container.append(testEl); 552 553 const element = testEl; 554 555 const visible = signal(true); 556 mount(container, { visible }); 557 558 await new Promise((resolve) => { 559 setTimeout(resolve, 100); 560 }); 561 562 expect(element.dataset.voltShiftRuns).toBe("1"); 563 }); 564 }); 565});