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 301 lines 11 kB view raw
1import { 2 applyOverrides, 3 easings, 4 getEasing, 5 getRegisteredTransitions, 6 getTransition, 7 hasTransition, 8 parseTransitionValue, 9 prefersReducedMotion, 10 registerTransition, 11 unregisterTransition, 12} from "$core/transitions"; 13import type { TransitionPreset } from "$types/volt"; 14import { describe, expect, it, vi } from "vitest"; 15 16describe("Transition Preset System", () => { 17 describe("Built-in Presets", () => { 18 it("should have fade preset registered", () => { 19 expect(hasTransition("fade")).toBe(true); 20 const fade = getTransition("fade"); 21 expect(fade).toBeDefined(); 22 expect(fade?.enter.from).toEqual({ opacity: 0 }); 23 expect(fade?.enter.to).toEqual({ opacity: 1 }); 24 expect(fade?.leave.from).toEqual({ opacity: 1 }); 25 expect(fade?.leave.to).toEqual({ opacity: 0 }); 26 }); 27 28 it.each([{ 29 name: "slide-up", 30 enterFrom: { opacity: 0, transform: "translateY(20px)" }, 31 enterTo: { opacity: 1, transform: "translateY(0)" }, 32 }, { 33 name: "slide-down", 34 enterFrom: { opacity: 0, transform: "translateY(-20px)" }, 35 enterTo: { opacity: 1, transform: "translateY(0)" }, 36 }, { 37 name: "slide-left", 38 enterFrom: { opacity: 0, transform: "translateX(20px)" }, 39 enterTo: { opacity: 1, transform: "translateX(0)" }, 40 }, { 41 name: "slide-right", 42 enterFrom: { opacity: 0, transform: "translateX(-20px)" }, 43 enterTo: { opacity: 1, transform: "translateX(0)" }, 44 }, { 45 name: "scale", 46 enterFrom: { opacity: 0, transform: "scale(0.95)" }, 47 enterTo: { opacity: 1, transform: "scale(1)" }, 48 }, { name: "blur", enterFrom: { opacity: 0, filter: "blur(10px)" }, enterTo: { opacity: 1, filter: "blur(0)" } }])( 49 "should have $name preset registered", 50 ({ name, enterFrom, enterTo }) => { 51 expect(hasTransition(name)).toBe(true); 52 const preset = getTransition(name); 53 expect(preset).toBeDefined(); 54 expect(preset?.enter.from).toEqual(enterFrom); 55 expect(preset?.enter.to).toEqual(enterTo); 56 }, 57 ); 58 59 it("should return all built-in preset names", () => { 60 const presets = getRegisteredTransitions(); 61 62 for (const preset of ["fade", "slide-up", "slide-down", "slide-left", "slide-right", "scale", "blur"]) { 63 expect(presets).toContain(preset); 64 } 65 }); 66 }); 67 68 describe("Custom Preset Registration", () => { 69 it("should register a custom transition preset", () => { 70 const customPreset: TransitionPreset = { 71 enter: { 72 from: { opacity: 0, transform: "translateX(-100px)" }, 73 to: { opacity: 1, transform: "translateX(0)" }, 74 duration: 400, 75 easing: "ease-out", 76 }, 77 leave: { 78 from: { opacity: 1, transform: "translateX(0)" }, 79 to: { opacity: 0, transform: "translateX(100px)" }, 80 duration: 300, 81 easing: "ease-in", 82 }, 83 }; 84 85 registerTransition("custom-slide", customPreset); 86 expect(hasTransition("custom-slide")).toBe(true); 87 88 const retrieved = getTransition("custom-slide"); 89 expect(retrieved).toEqual(customPreset); 90 }); 91 92 it("should unregister a custom preset", () => { 93 const customPreset: TransitionPreset = { enter: { from: {}, to: {} }, leave: { from: {}, to: {} } }; 94 95 registerTransition("temp-preset", customPreset); 96 expect(hasTransition("temp-preset")).toBe(true); 97 98 const result = unregisterTransition("temp-preset"); 99 expect(result).toBe(true); 100 expect(hasTransition("temp-preset")).toBe(false); 101 }); 102 103 it("should not unregister built-in presets", () => { 104 const result = unregisterTransition("fade"); 105 expect(result).toBe(false); 106 expect(hasTransition("fade")).toBe(true); 107 }); 108 109 it("should warn when overriding built-in preset", () => { 110 const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); 111 const customPreset: TransitionPreset = { enter: { from: {}, to: {} }, leave: { from: {}, to: {} } }; 112 113 registerTransition("fade", customPreset); 114 expect(consoleSpy).toHaveBeenCalledWith("[Volt] Overriding built-in transition preset: \"fade\""); 115 116 consoleSpy.mockRestore(); 117 }); 118 119 it("should return undefined for unknown preset", () => { 120 const preset = getTransition("nonexistent"); 121 expect(preset).toBeUndefined(); 122 }); 123 }); 124 125 describe("Parse Transition Value", () => { 126 it("should parse preset name only", () => { 127 const parsed = parseTransitionValue("fade"); 128 expect(parsed).toBeDefined(); 129 expect(parsed?.preset).toEqual(getTransition("fade")); 130 expect(parsed?.duration).toBeUndefined(); 131 expect(parsed?.delay).toBeUndefined(); 132 }); 133 134 it("should parse preset name with duration", () => { 135 const parsed = parseTransitionValue("fade.500"); 136 expect(parsed).toBeDefined(); 137 expect(parsed?.preset).toEqual(getTransition("fade")); 138 expect(parsed?.duration).toBe(500); 139 expect(parsed?.delay).toBeUndefined(); 140 }); 141 142 it("should parse preset name with duration and delay", () => { 143 const parsed = parseTransitionValue("slide-down.600.100"); 144 expect(parsed).toBeDefined(); 145 expect(parsed?.preset).toEqual(getTransition("slide-down")); 146 expect(parsed?.duration).toBe(600); 147 expect(parsed?.delay).toBe(100); 148 }); 149 150 it("should handle whitespace", () => { 151 const parsed = parseTransitionValue(" fade.500.100 "); 152 expect(parsed).toBeDefined(); 153 expect(parsed?.preset).toEqual(getTransition("fade")); 154 expect(parsed?.duration).toBe(500); 155 expect(parsed?.delay).toBe(100); 156 }); 157 158 it("should return undefined for empty string", () => { 159 const parsed = parseTransitionValue(""); 160 expect(parsed).toBeUndefined(); 161 }); 162 163 it("should return undefined for unknown preset", () => { 164 const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); 165 166 const parsed = parseTransitionValue("nonexistent"); 167 expect(parsed).toBeUndefined(); 168 expect(consoleSpy).toHaveBeenCalled(); 169 170 consoleSpy.mockRestore(); 171 }); 172 173 it("should ignore invalid duration values", () => { 174 const parsed = parseTransitionValue("fade.abc"); 175 expect(parsed).toBeDefined(); 176 expect(parsed?.duration).toBeUndefined(); 177 }); 178 179 it("should ignore invalid delay values", () => { 180 const parsed = parseTransitionValue("fade.500.xyz"); 181 expect(parsed).toBeDefined(); 182 expect(parsed?.duration).toBe(500); 183 expect(parsed?.delay).toBeUndefined(); 184 }); 185 }); 186 187 describe("Easing Functions", () => { 188 it("should return CSS easing for named easings", () => { 189 for (const e of ["linear", "ease", "ease-in", "ease-out", "ease-in-out"]) { 190 const res = getEasing(e); 191 expect(res).toEqual(e); 192 } 193 }); 194 195 it("should return cubic-bezier for named easing curves", () => { 196 expect(getEasing("ease-in-sine")).toBe("cubic-bezier(0.12, 0, 0.39, 0)"); 197 expect(getEasing("ease-out-sine")).toBe("cubic-bezier(0.61, 1, 0.88, 1)"); 198 expect(getEasing("ease-in-quad")).toBe("cubic-bezier(0.11, 0, 0.5, 0)"); 199 }); 200 201 it("should return custom cubic-bezier as-is", () => { 202 const custom = "cubic-bezier(0.25, 0.1, 0.25, 1)"; 203 expect(getEasing(custom)).toBe(custom); 204 }); 205 206 it("should have all easing constants defined", () => { 207 for ( 208 const prop of [ 209 "linear", 210 "ease", 211 "ease-in", 212 "ease-out", 213 "ease-in-out", 214 "ease-in-back", 215 "ease-out-back", 216 "ease-in-out-back", 217 ] 218 ) { 219 expect(easings).toHaveProperty(prop); 220 } 221 }); 222 }); 223 224 describe("Apply Overrides", () => { 225 it("should apply duration override", () => { 226 const phase = { from: { opacity: 0 }, to: { opacity: 1 }, duration: 300, delay: 0, easing: "ease" }; 227 const overridden = applyOverrides(phase, 500); 228 expect(overridden.duration).toBe(500); 229 expect(overridden.delay).toBe(0); 230 expect(overridden.from).toEqual({ opacity: 0 }); 231 expect(overridden.to).toEqual({ opacity: 1 }); 232 expect(overridden.easing).toBe("ease"); 233 }); 234 235 it("should apply delay override", () => { 236 const phase = { from: { opacity: 0 }, to: { opacity: 1 }, duration: 300, delay: 0, easing: "ease" }; 237 const overridden = applyOverrides(phase, undefined, 100); 238 expect(overridden.duration).toBe(300); 239 expect(overridden.delay).toBe(100); 240 }); 241 242 it("should apply both duration and delay overrides", () => { 243 const phase = { from: { opacity: 0 }, to: { opacity: 1 }, duration: 300, delay: 0, easing: "ease" }; 244 const overridden = applyOverrides(phase, 600, 200); 245 expect(overridden.duration).toBe(600); 246 expect(overridden.delay).toBe(200); 247 }); 248 249 it("should not mutate original phase", () => { 250 const phase = { from: { opacity: 0 }, to: { opacity: 1 }, duration: 300, delay: 0, easing: "ease" }; 251 const overridden = applyOverrides(phase, 500, 100); 252 expect(phase.duration).toBe(300); 253 expect(phase.delay).toBe(0); 254 expect(overridden).not.toBe(phase); 255 }); 256 257 it("should preserve all properties when no overrides", () => { 258 const phase = { 259 from: { opacity: 0, transform: "translateY(20px)" }, 260 to: { opacity: 1, transform: "translateY(0)" }, 261 duration: 300, 262 delay: 50, 263 easing: "ease-out", 264 classes: ["entering"], 265 }; 266 267 const overridden = applyOverrides(phase); 268 expect(overridden).toEqual(phase); 269 expect(overridden).not.toBe(phase); 270 }); 271 }); 272 273 describe("Prefers Reduced Motion", () => { 274 it("should return false when matchMedia is not available", () => { 275 const originalMatchMedia = globalThis.matchMedia; 276 // @ts-expect-error - Testing undefined case 277 delete globalThis.matchMedia; 278 279 expect(prefersReducedMotion()).toBe(false); 280 281 globalThis.matchMedia = originalMatchMedia; 282 }); 283 284 it("should check prefers-reduced-motion media query", () => { 285 const mockMatchMedia = vi.fn().mockReturnValue({ matches: true }); 286 globalThis.matchMedia = mockMatchMedia; 287 288 const result = prefersReducedMotion(); 289 290 expect(mockMatchMedia).toHaveBeenCalledWith("(prefers-reduced-motion: reduce)"); 291 expect(result).toBe(true); 292 }); 293 294 it("should return false when user does not prefer reduced motion", () => { 295 const mockMatchMedia = vi.fn().mockReturnValue({ matches: false }); 296 globalThis.matchMedia = mockMatchMedia; 297 const result = prefersReducedMotion(); 298 expect(result).toBe(false); 299 }); 300 }); 301});