import { shiftPlugin } from "$plugins/shift"; import { surgePlugin } from "$plugins/surge"; import type { TransitionPreset } from "$types/volt"; import { mount, registerPlugin, registerTransition, signal } from "$volt"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; describe("integration: transitions", () => { beforeEach(() => { registerPlugin("surge", surgePlugin); registerPlugin("shift", shiftPlugin); globalThis.matchMedia = vi.fn().mockReturnValue({ matches: false }); }); afterEach(() => { vi.restoreAllMocks(); }); describe("Surge with data-volt-if", () => { it("should animate element in when condition becomes true", async () => { vi.useFakeTimers(); const container = document.createElement("div"); container.innerHTML = `
Content
`; const show = signal(false); mount(container, { show }); const comment = [...container.childNodes].find((node) => node.nodeType === 8); expect(comment).toBeDefined(); show.set(true); await vi.advanceTimersByTimeAsync(400); const element = container.querySelector("div"); expect(element).toBeDefined(); if (element) { expect(element.textContent).toContain("Content"); } vi.useRealTimers(); }); it("should animate element out when condition becomes false", async () => { vi.useFakeTimers(); const container = document.createElement("div"); container.innerHTML = `
Content
`; const show = signal(true); mount(container, { show }); let element = container.querySelector("div"); expect(element).toBeDefined(); show.set(false); await vi.advanceTimersByTimeAsync(400); element = container.querySelector("div"); expect(element).toBeNull(); vi.useRealTimers(); }); it("should support custom enter/leave transitions", async () => { vi.useFakeTimers(); const container = document.createElement("div"); container.innerHTML = `
Content
`; const show = signal(false); mount(container, { show }); show.set(true); await vi.advanceTimersByTimeAsync(400); let element = container.querySelector("div"); expect(element).toBeDefined(); show.set(false); await vi.advanceTimersByTimeAsync(400); element = container.querySelector("div"); expect(element).toBeNull(); vi.useRealTimers(); }); it("should work with if/else pattern", async () => { vi.useFakeTimers(); const container = document.createElement("div"); container.innerHTML = `
Shown
Hidden
`; const show = signal(true); mount(container, { show }); await vi.advanceTimersByTimeAsync(400); let shownEl = [...container.querySelectorAll("div")].find((el) => el.textContent?.includes("Shown")); let hiddenEl = [...container.querySelectorAll("div")].find((el) => el.textContent?.includes("Hidden")); expect(shownEl).toBeDefined(); expect(hiddenEl).toBeUndefined(); show.set(false); await vi.advanceTimersByTimeAsync(400); shownEl = [...container.querySelectorAll("div")].find((el) => el.textContent?.includes("Shown")); hiddenEl = [...container.querySelectorAll("div")].find((el) => el.textContent?.includes("Hidden")); expect(shownEl).toBeUndefined(); expect(hiddenEl).toBeDefined(); vi.useRealTimers(); }); it("should support duration and delay modifiers", async () => { vi.useFakeTimers(); const container = document.createElement("div"); container.innerHTML = `
Content
`; const show = signal(false); mount(container, { show }); show.set(true); await vi.advanceTimersByTimeAsync(650); const element = container.querySelector("div"); expect(element).toBeDefined(); vi.useRealTimers(); }); }); describe("Surge with data-volt-show", () => { it("should toggle display property with transition", async () => { vi.useFakeTimers(); const container = document.createElement("div"); const testEl = document.createElement("div"); testEl.dataset.voltShow = "visible"; testEl.dataset.voltSurge = "fade"; testEl.textContent = "Content"; container.append(testEl); const visible = signal(true); const element = testEl; mount(container, { visible }); expect(element.style.display).not.toBe("none"); visible.set(false); await vi.advanceTimersByTimeAsync(400); expect(element.style.display).toBe("none"); visible.set(true); await vi.advanceTimersByTimeAsync(400); expect(element.style.display).not.toBe("none"); vi.useRealTimers(); }); it("should not start overlapping transitions", async () => { vi.useFakeTimers(); const container = document.createElement("div"); container.innerHTML = `
Content
`; const visible = signal(true); mount(container, { visible }); const element = container.querySelector("div") as HTMLElement; visible.set(false); visible.set(true); visible.set(false); await vi.advanceTimersByTimeAsync(50); expect(element).toBeDefined(); vi.useRealTimers(); }); }); describe("Surge signal-triggered mode", () => { it("should watch signal and apply transitions", async () => { vi.useFakeTimers(); const container = document.createElement("div"); const testEl = document.createElement("div"); testEl.dataset.voltSurge = "show:fade"; testEl.textContent = "Content"; container.append(testEl); const show = signal(false); const element = testEl; mount(container, { show }); expect(element.style.display).toBe("none"); show.set(true); await vi.advanceTimersByTimeAsync(400); expect(element.style.display).not.toBe("none"); show.set(false); await vi.advanceTimersByTimeAsync(400); expect(element.style.display).toBe("none"); vi.useRealTimers(); }); it("should cleanup subscription on unmount", async () => { const container = document.createElement("div"); const testEl = document.createElement("div"); testEl.dataset.voltSurge = "show:fade"; testEl.textContent = "Content"; container.append(testEl); const show = signal(false); const element = testEl; const cleanup = mount(container, { show }); expect(element.style.display).toBe("none"); cleanup(); const initialDisplay = element.style.display; show.set(true); await new Promise((resolve) => { setTimeout(resolve, 50); }); expect(element.style.display).toBe(initialDisplay); }); }); describe("Shift animations", () => { it("should apply animation on mount", async () => { const container = document.createElement("div"); const testEl = document.createElement("div"); testEl.dataset.voltShift = "bounce"; testEl.textContent = "Content"; container.append(testEl); const element = testEl; mount(container, {}); // Wait for requestAnimationFrame to apply the animation await vi.waitFor(() => { expect(element.dataset.voltShiftRuns).toBe("1"); expect(element.style.animationName).toMatch(/^volt-shift-/); }); }); it("should trigger animation based on signal", () => { const container = document.createElement("div"); container.innerHTML = ` `; const trigger = signal(false); mount(container, { trigger }); const button = container.querySelector("button") as HTMLElement; expect(button.dataset.voltShiftRuns ?? "0").toBe("0"); trigger.set(true); expect(button.dataset.voltShiftRuns).toBe("1"); }); it("should support duration and iteration modifiers", async () => { const container = document.createElement("div"); const testEl = document.createElement("div"); testEl.dataset.voltShift = "bounce.1000.3"; testEl.textContent = "Content"; container.append(testEl); const element = testEl; mount(container, {}); // Wait for requestAnimationFrame to apply the animation await vi.waitFor(() => { expect(element.dataset.voltShiftRuns).toBe("1"); expect(element.style.animationDuration).toBe("1000ms"); expect(element.style.animationIterationCount).toBe("3"); }); }); it("should cleanup signal subscription on unmount", () => { const container = document.createElement("div"); container.innerHTML = ` `; const trigger = signal(false); const cleanup = mount(container, { trigger }); cleanup(); const button = container.querySelector("button") as HTMLElement; trigger.set(true); expect(button.dataset.voltShiftRuns ?? "0").toBe("0"); }); }); describe("Custom presets", () => { it("should use registered custom transition preset", async () => { vi.useFakeTimers(); const customPreset: TransitionPreset = { enter: { from: { opacity: 0, transform: "scale(0.5)" }, to: { opacity: 1, transform: "scale(1)" }, duration: 200, easing: "ease-out", }, leave: { from: { opacity: 1, transform: "scale(1)" }, to: { opacity: 0, transform: "scale(0.5)" }, duration: 200, easing: "ease-in", }, }; registerTransition("custom-scale", customPreset); const container = document.createElement("div"); container.innerHTML = `
Content
`; const show = signal(false); mount(container, { show }); show.set(true); await vi.advanceTimersByTimeAsync(300); const element = container.querySelector("div"); expect(element).toBeDefined(); vi.useRealTimers(); }); }); describe("Accessibility: prefers-reduced-motion", () => { it("should skip animations when user prefers reduced motion", async () => { globalThis.matchMedia = vi.fn().mockReturnValue({ matches: true }); const container = document.createElement("div"); container.innerHTML = `
Content
`; const show = signal(false); mount(container, { show }); show.set(true); await new Promise((resolve) => { setTimeout(resolve, 50); }); const element = container.querySelector("div"); expect(element).toBeDefined(); }); }); describe("View Transitions API", () => { it("should use View Transitions API when available", async () => { const mockStartViewTransition = vi.fn((callback: () => void | Promise) => { const result = callback(); return { finished: Promise.resolve(result).then(() => {}), ready: Promise.resolve(), updateCallbackDone: Promise.resolve(result).then(() => {}), skipTransition: vi.fn(), }; }); // @ts-expect-error - Adding View Transitions API mock document.startViewTransition = mockStartViewTransition; const { startViewTransition } = await import("$core/view-transitions"); await startViewTransition(() => { const el = document.createElement("div"); el.textContent = "test"; }); expect(mockStartViewTransition).toHaveBeenCalled(); // @ts-expect-error - Cleanup mock delete document.startViewTransition; }); it("should fallback to CSS when View Transitions API not available", async () => { // @ts-expect-error - Ensure View Transitions API is not available delete document.startViewTransition; vi.useFakeTimers(); const container = document.createElement("div"); container.innerHTML = `
Content
`; const show = signal(false); mount(container, { show }); show.set(true); await vi.advanceTimersByTimeAsync(400); const element = container.querySelector("div"); expect(element).toBeDefined(); vi.useRealTimers(); }); }); describe("Memory leak prevention", () => { it("should cleanup all transition-related subscriptions", async () => { const container = document.createElement("div"); container.innerHTML = `
Content 1
Content 2
Content 3
`; const show = signal(false); const visible = signal(true); const trigger = signal(false); const animTrigger = signal(false); const cleanup = mount(container, { show, visible, trigger, animTrigger }); cleanup(); const initialHTML = container.innerHTML; show.set(true); visible.set(false); trigger.set(true); animTrigger.set(true); await new Promise((resolve) => { setTimeout(resolve, 50); }); expect(container.innerHTML).toBe(initialHTML); }); }); describe("Complex integration scenarios", () => { it("should handle multiple animated elements simultaneously", async () => { vi.useFakeTimers(); const container = document.createElement("div"); container.innerHTML = `
Item 1
Item 2
Item 3
`; const show1 = signal(false); const show2 = signal(false); const show3 = signal(false); mount(container, { show1, show2, show3 }); show1.set(true); show2.set(true); show3.set(true); await vi.advanceTimersByTimeAsync(400); const elements = container.querySelectorAll("div"); expect(elements.length).toBe(3); vi.useRealTimers(); }); it("should combine surge and shift on same element", async () => { const container = document.createElement("div"); const testEl = document.createElement("div"); testEl.dataset.voltShow = "visible"; testEl.dataset.voltSurge = "fade"; testEl.dataset.voltShift = "bounce"; testEl.textContent = "Combined"; container.append(testEl); const element = testEl; const visible = signal(true); mount(container, { visible }); await new Promise((resolve) => { setTimeout(resolve, 100); }); expect(element.dataset.voltShiftRuns).toBe("1"); }); }); });