Files
novalon-website/src/lib/animations.test.tsx
T
张翔 84f488a253 fix(types): 修复 16 个 TypeScript 类型检查错误
- 修复 animations.test.tsx 中的 Variant 类型访问问题
- 清理 9 个测试文件中的未使用导入
- 使用可选链操作符处理可能为 undefined 的属性访问
- 修复 mock 组件缺少 displayName 的 ESLint 错误
2026-04-22 19:47:52 +08:00

540 lines
20 KiB
TypeScript

import { describe, it, expect, jest } from '@jest/globals';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, initial, animate, variants, className, whileHover, whileTap, ...props }: any) => (
<div
data-testid="motion-div"
data-initial={JSON.stringify(initial)}
data-animate={JSON.stringify(animate)}
data-variants={JSON.stringify(variants)}
data-while-hover={JSON.stringify(whileHover)}
data-while-tap={JSON.stringify(whileTap)}
className={className}
{...props}
>
{children}
</div>
),
button: ({ children, onClick, className, whileHover, whileTap, ...props }: any) => (
<button
data-testid="motion-button"
onClick={onClick}
className={className}
data-while-hover={JSON.stringify(whileHover)}
data-while-tap={JSON.stringify(whileTap)}
{...props}
>
{children}
</button>
),
span: ({ children, className, animate, ...props }: any) => (
<span
data-testid="motion-span"
className={className}
data-animate={JSON.stringify(animate)}
{...props}
>
{children}
</span>
),
svg: ({ children, className, ...props }: any) => (
<svg data-testid="motion-svg" className={className} {...props}>
{children}
</svg>
),
circle: ({ variants, ...props }: any) => (
<circle data-testid="motion-circle" data-variants={JSON.stringify(variants)} {...props} />
),
path: ({ variants, ...props }: any) => (
<path data-testid="motion-path" data-variants={JSON.stringify(variants)} {...props} />
),
},
useInView: jest.fn(() => true),
useSpring: jest.fn((value) => value),
useTransform: jest.fn((value) => value),
}));
describe('Animation Variants', () => {
describe('inkVariants', () => {
it('should have correct hidden state', async () => {
const { inkVariants } = await import('./animations');
expect(inkVariants.hidden).toEqual({
opacity: 0,
scale: 0.8,
filter: 'blur(10px)',
});
});
it('should have correct visible state', async () => {
const { inkVariants } = await import('./animations');
expect(inkVariants.visible).toHaveProperty('opacity', 1);
expect(inkVariants.visible).toHaveProperty('scale', 1);
expect(inkVariants.visible).toHaveProperty('filter', 'blur(0px)');
});
it('should have correct transition configuration', async () => {
const { inkVariants } = await import('./animations');
const transition = (inkVariants.visible as any)?.transition;
expect(transition.duration).toBe(0.8);
expect(transition.ease).toEqual([0.16, 1, 0.3, 1]);
});
});
describe('sealStampVariants', () => {
it('should have correct hidden state', async () => {
const { sealStampVariants } = await import('./animations');
expect(sealStampVariants.hidden).toEqual({
opacity: 0,
scale: 1.5,
rotate: -15,
});
});
it('should have correct visible state', async () => {
const { sealStampVariants } = await import('./animations');
expect(sealStampVariants.visible).toHaveProperty('opacity', 1);
expect(sealStampVariants.visible).toHaveProperty('scale', 1);
expect(sealStampVariants.visible).toHaveProperty('rotate', 0);
});
it('should use spring animation', async () => {
const { sealStampVariants } = await import('./animations');
const transition = (sealStampVariants.visible as any)?.transition;
expect(transition.type).toBe('spring');
expect(transition.stiffness).toBe(300);
expect(transition.damping).toBe(20);
});
});
describe('brushStrokeVariants', () => {
it('should have correct hidden state', async () => {
const { brushStrokeVariants } = await import('./animations');
expect(brushStrokeVariants.hidden).toEqual({
pathLength: 0,
opacity: 0,
});
});
it('should have correct visible state', async () => {
const { brushStrokeVariants } = await import('./animations');
expect(brushStrokeVariants.visible).toHaveProperty('pathLength', 1);
expect(brushStrokeVariants.visible).toHaveProperty('opacity', 1);
});
});
describe('fadeUpVariants', () => {
it('should have correct hidden state', async () => {
const { fadeUpVariants } = await import('./animations');
expect(fadeUpVariants.hidden).toEqual({
opacity: 0,
y: 30,
});
});
it('should have correct visible state', async () => {
const { fadeUpVariants } = await import('./animations');
expect(fadeUpVariants.visible).toHaveProperty('opacity', 1);
expect(fadeUpVariants.visible).toHaveProperty('y', 0);
});
});
describe('staggerContainerVariants', () => {
it('should have staggerChildren configured', async () => {
const { staggerContainerVariants } = await import('./animations');
const transition = (staggerContainerVariants.visible as any)?.transition;
expect(transition.staggerChildren).toBe(0.1);
expect(transition.delayChildren).toBe(0.1);
});
});
describe('staggerItemVariants', () => {
it('should have correct hidden state', async () => {
const { staggerItemVariants } = await import('./animations');
expect(staggerItemVariants.hidden).toEqual({
opacity: 0,
y: 20,
scale: 0.95,
});
});
it('should have correct visible state', async () => {
const { staggerItemVariants } = await import('./animations');
expect(staggerItemVariants.visible).toHaveProperty('opacity', 1);
expect(staggerItemVariants.visible).toHaveProperty('y', 0);
expect(staggerItemVariants.visible).toHaveProperty('scale', 1);
});
});
});
describe('Animation Components', () => {
describe('InkReveal', () => {
it('should render children correctly', async () => {
const { InkReveal } = await import('./animations');
render(<InkReveal>Test Content</InkReveal>);
expect(screen.getByText('Test Content')).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { InkReveal } = await import('./animations');
render(<InkReveal className="custom-class">Test</InkReveal>);
const element = screen.getByTestId('motion-div');
expect(element).toHaveClass('custom-class');
});
it('should use inkVariants', async () => {
const { InkReveal, inkVariants } = await import('./animations');
render(<InkReveal>Test</InkReveal>);
const element = screen.getByTestId('motion-div');
const variants = JSON.parse(element.getAttribute('data-variants') || '{}');
expect(variants).toEqual(inkVariants);
});
});
describe('SealStamp', () => {
it('should render children correctly', async () => {
const { SealStamp } = await import('./animations');
render(<SealStamp>Seal Content</SealStamp>);
expect(screen.getByText('Seal Content')).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { SealStamp } = await import('./animations');
render(<SealStamp className="seal-class">Test</SealStamp>);
const element = screen.getByTestId('motion-div');
expect(element).toHaveClass('seal-class');
});
});
describe('FadeUp', () => {
it('should render children correctly', async () => {
const { FadeUp } = await import('./animations');
render(<FadeUp>Fade Content</FadeUp>);
expect(screen.getByText('Fade Content')).toBeInTheDocument();
});
it('should apply custom duration', async () => {
const { FadeUp } = await import('./animations');
render(<FadeUp duration={1.2}>Test</FadeUp>);
const element = screen.getByTestId('motion-div');
const variants = JSON.parse(element.getAttribute('data-variants') || '{}');
expect(variants.visible.transition.duration).toBe(1.2);
});
it('should apply delay prop', async () => {
const { FadeUp } = await import('./animations');
render(<FadeUp delay={0.3}>Test</FadeUp>);
const element = screen.getByTestId('motion-div');
const variants = JSON.parse(element.getAttribute('data-variants') || '{}');
expect(variants.visible.transition.delay).toBe(0.3);
});
});
describe('StaggerContainer', () => {
it('should render children correctly', async () => {
const { StaggerContainer } = await import('./animations');
render(
<StaggerContainer>
<div>Item 1</div>
<div>Item 2</div>
</StaggerContainer>
);
expect(screen.getByText('Item 1')).toBeInTheDocument();
expect(screen.getByText('Item 2')).toBeInTheDocument();
});
it('should apply custom staggerDelay', async () => {
const { StaggerContainer } = await import('./animations');
render(<StaggerContainer staggerDelay={0.2}>Test</StaggerContainer>);
const element = screen.getByTestId('motion-div');
const variants = JSON.parse(element.getAttribute('data-variants') || '{}');
expect(variants.visible.transition.staggerChildren).toBe(0.2);
});
});
describe('StaggerItem', () => {
it('should render children correctly', async () => {
const { StaggerItem } = await import('./animations');
render(<StaggerItem>Item Content</StaggerItem>);
expect(screen.getByText('Item Content')).toBeInTheDocument();
});
});
describe('RippleButton', () => {
it('should render children correctly', async () => {
const { RippleButton } = await import('./animations');
render(<RippleButton>Click Me</RippleButton>);
expect(screen.getByText('Click Me')).toBeInTheDocument();
});
it('should handle click events', async () => {
const { RippleButton } = await import('./animations');
const handleClick = jest.fn();
render(<RippleButton onClick={handleClick}>Click Me</RippleButton>);
const button = screen.getByTestId('motion-button');
fireEvent.click(button);
expect(handleClick).toHaveBeenCalled();
});
it('should apply custom className', async () => {
const { RippleButton } = await import('./animations');
render(<RippleButton className="custom-button">Test</RippleButton>);
const element = screen.getByTestId('motion-button');
expect(element).toHaveClass('custom-button');
});
});
describe('InkCard', () => {
it('should render children correctly', async () => {
const { InkCard } = await import('./animations');
render(<InkCard>Card Content</InkCard>);
expect(screen.getByText('Card Content')).toBeInTheDocument();
});
it('should apply custom hoverScale', async () => {
const { InkCard } = await import('./animations');
render(<InkCard hoverScale={1.1}>Test</InkCard>);
const element = screen.getByTestId('motion-div');
const whileHover = JSON.parse(element.getAttribute('data-while-hover') || '{}');
expect(whileHover.scale).toBe(1.1);
});
});
describe('CountUp', () => {
it('should render with prefix and suffix', async () => {
const { CountUp } = await import('./animations');
render(<CountUp end={100} prefix="$" suffix="%" />);
expect(screen.getByText(/\$/)).toBeInTheDocument();
expect(screen.getByText(/%/)).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { CountUp } = await import('./animations');
render(<CountUp end={100} className="counter-class" />);
const element = screen.getByTestId('motion-span');
expect(element).toHaveClass('counter-class');
});
});
describe('Typewriter', () => {
it('should render component correctly', async () => {
const { Typewriter } = await import('./animations');
render(<Typewriter text="Hello" />);
const cursor = screen.getByText('|');
expect(cursor).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { Typewriter } = await import('./animations');
render(<Typewriter text="Test" className="typewriter-class" />);
const container = screen.getByText('|').closest('.typewriter-class');
expect(container).toBeInTheDocument();
});
});
describe('FloatingElement', () => {
it('should render children correctly', async () => {
const { FloatingElement } = await import('./animations');
render(<FloatingElement>Floating Content</FloatingElement>);
expect(screen.getByText('Floating Content')).toBeInTheDocument();
});
it('should apply custom amplitude', async () => {
const { FloatingElement } = await import('./animations');
render(<FloatingElement amplitude={20}>Test</FloatingElement>);
const element = screen.getByTestId('motion-div');
const animate = JSON.parse(element.getAttribute('data-animate') || '{}');
expect(animate.y).toEqual([-20, 20, -20]);
});
});
describe('PulseElement', () => {
it('should render children correctly', async () => {
const { PulseElement } = await import('./animations');
render(<PulseElement>Pulse Content</PulseElement>);
expect(screen.getByText('Pulse Content')).toBeInTheDocument();
});
it('should apply custom scale', async () => {
const { PulseElement } = await import('./animations');
render(<PulseElement scale={1.1}>Test</PulseElement>);
const element = screen.getByTestId('motion-div');
const animate = JSON.parse(element.getAttribute('data-animate') || '{}');
expect(animate.scale).toEqual([1, 1.1, 1]);
});
});
describe('GradientText', () => {
it('should render children correctly', async () => {
const { GradientText } = await import('./animations');
render(<GradientText>Gradient Text</GradientText>);
expect(screen.getByText('Gradient Text')).toBeInTheDocument();
});
it('should apply custom colors', async () => {
const { GradientText } = await import('./animations');
render(<GradientText colors={['#ff0000', '#00ff00', '#0000ff']}>Test</GradientText>);
const element = screen.getByTestId('motion-span');
expect(element).toBeInTheDocument();
});
});
describe('SplitText', () => {
it('should render text correctly', async () => {
const { SplitText } = await import('./animations');
render(<SplitText text="Hi" />);
expect(screen.getByText('H')).toBeInTheDocument();
expect(screen.getByText('i')).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { SplitText } = await import('./animations');
render(<SplitText text="Test" className="split-class" />);
const elements = screen.getAllByTestId('motion-span');
const parentElement = elements.find(el => el.classList.contains('split-class'));
expect(parentElement).toBeDefined();
});
});
describe('GlitchText', () => {
it('should render text correctly', async () => {
const { GlitchText } = await import('./animations');
render(<GlitchText text="Glitch" />);
const glitchElements = screen.getAllByText('Glitch');
expect(glitchElements.length).toBeGreaterThan(0);
});
it('should apply custom className', async () => {
const { GlitchText } = await import('./animations');
render(<GlitchText text="Test" className="glitch-class" />);
const testElements = screen.getAllByText('Test');
const container = testElements[0]?.closest('.glitch-class');
expect(container).toBeInTheDocument();
});
});
describe('MagneticButton', () => {
it('should render children correctly', async () => {
const { MagneticButton } = await import('./animations');
render(<MagneticButton>Magnetic</MagneticButton>);
expect(screen.getByText('Magnetic')).toBeInTheDocument();
});
it('should handle click events', async () => {
const { MagneticButton } = await import('./animations');
const handleClick = jest.fn();
render(<MagneticButton onClick={handleClick}>Click</MagneticButton>);
const element = screen.getByText('Click');
fireEvent.click(element);
expect(handleClick).toHaveBeenCalled();
});
});
describe('BlurReveal', () => {
it('should render children correctly', async () => {
const { BlurReveal } = await import('./animations');
render(<BlurReveal>Blur Content</BlurReveal>);
expect(screen.getByText('Blur Content')).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { BlurReveal } = await import('./animations');
render(<BlurReveal className="blur-class">Test</BlurReveal>);
const element = screen.getByTestId('motion-div');
expect(element).toHaveClass('blur-class');
});
});
describe('WaveText', () => {
it('should render text correctly', async () => {
const { WaveText } = await import('./animations');
render(<WaveText text="Hi" />);
expect(screen.getByText('H')).toBeInTheDocument();
expect(screen.getByText('i')).toBeInTheDocument();
});
});
describe('ShimmerButton', () => {
it('should render children correctly', async () => {
const { ShimmerButton } = await import('./animations');
render(<ShimmerButton>Shimmer</ShimmerButton>);
expect(screen.getByText('Shimmer')).toBeInTheDocument();
});
it('should handle click events', async () => {
const { ShimmerButton } = await import('./animations');
const handleClick = jest.fn();
render(<ShimmerButton onClick={handleClick}>Click</ShimmerButton>);
const button = screen.getByTestId('motion-button');
fireEvent.click(button);
expect(handleClick).toHaveBeenCalled();
});
});
});
describe('Animation Hooks', () => {
describe('useParallax', () => {
it('should be defined', async () => {
const { useParallax } = await import('./animations');
expect(useParallax).toBeDefined();
});
});
describe('useSmoothSpring', () => {
it('should be defined', async () => {
const { useSmoothSpring } = await import('./animations');
expect(useSmoothSpring).toBeDefined();
});
});
});
describe('SVG Components', () => {
describe('InkDropSVG', () => {
it('should render SVG correctly', async () => {
const { InkDropSVG } = await import('./animations');
render(<InkDropSVG />);
expect(screen.getByTestId('motion-svg')).toBeInTheDocument();
expect(screen.getByTestId('motion-circle')).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { InkDropSVG } = await import('./animations');
render(<InkDropSVG className="ink-drop-class" />);
const element = screen.getByTestId('motion-svg');
expect(element).toHaveClass('ink-drop-class');
});
});
describe('InkSplash', () => {
it('should render SVG correctly', async () => {
const { InkSplash } = await import('./animations');
render(<InkSplash />);
expect(screen.getByTestId('motion-svg')).toBeInTheDocument();
expect(screen.getByTestId('motion-path')).toBeInTheDocument();
});
it('should apply custom color', async () => {
const { InkSplash } = await import('./animations');
render(<InkSplash color="#ff0000" />);
const path = screen.getByTestId('motion-path');
expect(path).toHaveAttribute('fill', '#ff0000');
});
it('should apply custom size', async () => {
const { InkSplash } = await import('./animations');
render(<InkSplash size={200} />);
const svg = screen.getByTestId('motion-svg');
expect(svg).toHaveAttribute('width', '200');
expect(svg).toHaveAttribute('height', '200');
});
});
});