|
| 1 | +import { describe, it, expect } from 'vitest'; |
| 2 | +import { |
| 3 | + TransitionPresetSchema, |
| 4 | + EasingFunctionSchema, |
| 5 | + TransitionConfigSchema, |
| 6 | + AnimationTriggerSchema, |
| 7 | + ComponentAnimationSchema, |
| 8 | + PageTransitionSchema, |
| 9 | + MotionConfigSchema, |
| 10 | + type TransitionPreset, |
| 11 | + type EasingFunction, |
| 12 | + type TransitionConfig, |
| 13 | + type AnimationTrigger, |
| 14 | + type ComponentAnimation, |
| 15 | + type PageTransition, |
| 16 | + type MotionConfig, |
| 17 | +} from './animation.zod'; |
| 18 | + |
| 19 | +describe('TransitionPresetSchema', () => { |
| 20 | + it('should accept all valid presets', () => { |
| 21 | + const presets = ['fade', 'slide_up', 'slide_down', 'slide_left', 'slide_right', 'scale', 'rotate', 'flip', 'none'] as const; |
| 22 | + presets.forEach(preset => { |
| 23 | + expect(() => TransitionPresetSchema.parse(preset)).not.toThrow(); |
| 24 | + }); |
| 25 | + }); |
| 26 | + |
| 27 | + it('should reject invalid presets', () => { |
| 28 | + expect(() => TransitionPresetSchema.parse('dissolve')).toThrow(); |
| 29 | + expect(() => TransitionPresetSchema.parse('')).toThrow(); |
| 30 | + }); |
| 31 | +}); |
| 32 | + |
| 33 | +describe('EasingFunctionSchema', () => { |
| 34 | + it('should accept all valid easing functions', () => { |
| 35 | + const easings = ['linear', 'ease', 'ease_in', 'ease_out', 'ease_in_out', 'spring'] as const; |
| 36 | + easings.forEach(easing => { |
| 37 | + expect(() => EasingFunctionSchema.parse(easing)).not.toThrow(); |
| 38 | + }); |
| 39 | + }); |
| 40 | + |
| 41 | + it('should reject invalid easing functions', () => { |
| 42 | + expect(() => EasingFunctionSchema.parse('bounce')).toThrow(); |
| 43 | + expect(() => EasingFunctionSchema.parse('')).toThrow(); |
| 44 | + }); |
| 45 | +}); |
| 46 | + |
| 47 | +describe('TransitionConfigSchema', () => { |
| 48 | + it('should accept empty config', () => { |
| 49 | + const result = TransitionConfigSchema.parse({}); |
| 50 | + expect(result).toEqual({}); |
| 51 | + }); |
| 52 | + |
| 53 | + it('should accept full config with all fields', () => { |
| 54 | + const config: TransitionConfig = { |
| 55 | + preset: 'fade', |
| 56 | + duration: 200, |
| 57 | + easing: 'ease_in_out', |
| 58 | + delay: 50, |
| 59 | + customKeyframes: 'bounce-in', |
| 60 | + }; |
| 61 | + const result = TransitionConfigSchema.parse(config); |
| 62 | + expect(result.preset).toBe('fade'); |
| 63 | + expect(result.duration).toBe(200); |
| 64 | + expect(result.easing).toBe('ease_in_out'); |
| 65 | + expect(result.delay).toBe(50); |
| 66 | + expect(result.customKeyframes).toBe('bounce-in'); |
| 67 | + }); |
| 68 | + |
| 69 | + it('should leave optional fields undefined when not provided', () => { |
| 70 | + const result = TransitionConfigSchema.parse({ duration: 100 }); |
| 71 | + expect(result.duration).toBe(100); |
| 72 | + expect(result.preset).toBeUndefined(); |
| 73 | + expect(result.easing).toBeUndefined(); |
| 74 | + expect(result.delay).toBeUndefined(); |
| 75 | + expect(result.customKeyframes).toBeUndefined(); |
| 76 | + }); |
| 77 | +}); |
| 78 | + |
| 79 | +describe('AnimationTriggerSchema', () => { |
| 80 | + it('should accept all valid triggers', () => { |
| 81 | + const triggers = ['on_mount', 'on_unmount', 'on_hover', 'on_focus', 'on_click', 'on_scroll', 'on_visible'] as const; |
| 82 | + triggers.forEach(trigger => { |
| 83 | + expect(() => AnimationTriggerSchema.parse(trigger)).not.toThrow(); |
| 84 | + }); |
| 85 | + }); |
| 86 | + |
| 87 | + it('should reject invalid triggers', () => { |
| 88 | + expect(() => AnimationTriggerSchema.parse('on_drag')).toThrow(); |
| 89 | + expect(() => AnimationTriggerSchema.parse('')).toThrow(); |
| 90 | + }); |
| 91 | +}); |
| 92 | + |
| 93 | +describe('ComponentAnimationSchema', () => { |
| 94 | + it('should apply default reducedMotion for empty config', () => { |
| 95 | + const result = ComponentAnimationSchema.parse({}); |
| 96 | + expect(result.reducedMotion).toBe('respect'); |
| 97 | + }); |
| 98 | + |
| 99 | + it('should accept full config with enter/exit/hover/trigger/reducedMotion', () => { |
| 100 | + const config: ComponentAnimation = { |
| 101 | + enter: { preset: 'slide_up', duration: 300, easing: 'ease_out' }, |
| 102 | + exit: { preset: 'fade', duration: 200 }, |
| 103 | + hover: { preset: 'scale', duration: 150 }, |
| 104 | + trigger: 'on_visible', |
| 105 | + reducedMotion: 'alternative', |
| 106 | + }; |
| 107 | + const result = ComponentAnimationSchema.parse(config); |
| 108 | + expect(result.enter?.preset).toBe('slide_up'); |
| 109 | + expect(result.exit?.preset).toBe('fade'); |
| 110 | + expect(result.hover?.preset).toBe('scale'); |
| 111 | + expect(result.trigger).toBe('on_visible'); |
| 112 | + expect(result.reducedMotion).toBe('alternative'); |
| 113 | + }); |
| 114 | + |
| 115 | + it('should accept disable for reducedMotion', () => { |
| 116 | + const result = ComponentAnimationSchema.parse({ reducedMotion: 'disable' }); |
| 117 | + expect(result.reducedMotion).toBe('disable'); |
| 118 | + }); |
| 119 | +}); |
| 120 | + |
| 121 | +describe('PageTransitionSchema', () => { |
| 122 | + it('should apply defaults for empty config', () => { |
| 123 | + const result = PageTransitionSchema.parse({}); |
| 124 | + expect(result.type).toBe('fade'); |
| 125 | + expect(result.duration).toBe(300); |
| 126 | + expect(result.easing).toBe('ease_in_out'); |
| 127 | + expect(result.crossFade).toBe(false); |
| 128 | + }); |
| 129 | + |
| 130 | + it('should accept full config overriding defaults', () => { |
| 131 | + const config: PageTransition = { |
| 132 | + type: 'slide_left', |
| 133 | + duration: 500, |
| 134 | + easing: 'spring', |
| 135 | + crossFade: true, |
| 136 | + }; |
| 137 | + const result = PageTransitionSchema.parse(config); |
| 138 | + expect(result.type).toBe('slide_left'); |
| 139 | + expect(result.duration).toBe(500); |
| 140 | + expect(result.easing).toBe('spring'); |
| 141 | + expect(result.crossFade).toBe(true); |
| 142 | + }); |
| 143 | +}); |
| 144 | + |
| 145 | +describe('MotionConfigSchema', () => { |
| 146 | + it('should apply defaults for empty config', () => { |
| 147 | + const result = MotionConfigSchema.parse({}); |
| 148 | + expect(result.enabled).toBe(true); |
| 149 | + expect(result.reducedMotion).toBe(false); |
| 150 | + }); |
| 151 | + |
| 152 | + it('should accept full config with componentAnimations record', () => { |
| 153 | + const config: MotionConfig = { |
| 154 | + defaultTransition: { preset: 'fade', duration: 250, easing: 'ease' }, |
| 155 | + pageTransitions: { type: 'slide_right', duration: 400, easing: 'ease_in_out', crossFade: false }, |
| 156 | + componentAnimations: { |
| 157 | + card: { enter: { preset: 'scale', duration: 200 }, reducedMotion: 'respect' }, |
| 158 | + modal: { enter: { preset: 'slide_up' }, exit: { preset: 'fade' }, reducedMotion: 'disable' }, |
| 159 | + }, |
| 160 | + reducedMotion: true, |
| 161 | + enabled: false, |
| 162 | + }; |
| 163 | + const result = MotionConfigSchema.parse(config); |
| 164 | + expect(result.defaultTransition?.preset).toBe('fade'); |
| 165 | + expect(result.pageTransitions?.type).toBe('slide_right'); |
| 166 | + expect(result.componentAnimations?.card.enter?.preset).toBe('scale'); |
| 167 | + expect(result.componentAnimations?.modal.reducedMotion).toBe('disable'); |
| 168 | + expect(result.reducedMotion).toBe(true); |
| 169 | + expect(result.enabled).toBe(false); |
| 170 | + }); |
| 171 | + |
| 172 | + it('should leave optional fields undefined when not provided', () => { |
| 173 | + const result = MotionConfigSchema.parse({}); |
| 174 | + expect(result.defaultTransition).toBeUndefined(); |
| 175 | + expect(result.pageTransitions).toBeUndefined(); |
| 176 | + expect(result.componentAnimations).toBeUndefined(); |
| 177 | + }); |
| 178 | +}); |
| 179 | + |
| 180 | +describe('Type exports', () => { |
| 181 | + it('should have valid type exports', () => { |
| 182 | + const preset: TransitionPreset = 'fade'; |
| 183 | + const easing: EasingFunction = 'linear'; |
| 184 | + const transition: TransitionConfig = {}; |
| 185 | + const trigger: AnimationTrigger = 'on_mount'; |
| 186 | + const component: ComponentAnimation = { reducedMotion: 'respect' }; |
| 187 | + const page: PageTransition = { type: 'fade', duration: 300, easing: 'ease_in_out', crossFade: false }; |
| 188 | + const motion: MotionConfig = { reducedMotion: false, enabled: true }; |
| 189 | + expect(preset).toBeDefined(); |
| 190 | + expect(easing).toBeDefined(); |
| 191 | + expect(transition).toBeDefined(); |
| 192 | + expect(trigger).toBeDefined(); |
| 193 | + expect(component).toBeDefined(); |
| 194 | + expect(page).toBeDefined(); |
| 195 | + expect(motion).toBeDefined(); |
| 196 | + }); |
| 197 | +}); |
0 commit comments