chore: remove GitHub Actions workflows, use Woodpecker CI exclusively

This commit is contained in:
张翔
2026-03-10 13:10:11 +08:00
parent 0a1adfc2a2
commit e8dffa4f05
82 changed files with 19565 additions and 101 deletions
+95
View File
@@ -0,0 +1,95 @@
jest.mock('./analytics', () => {
const actual = jest.requireActual('./analytics');
return {
...actual,
pageview: jest.fn(),
event: jest.fn(),
trackContactForm: jest.fn(),
trackButtonClick: jest.fn(),
trackPageView: jest.fn(),
};
});
import {
pageview,
event,
trackContactForm,
trackButtonClick,
trackPageView,
} from './analytics';
describe('analytics', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('pageview', () => {
it('should be defined', () => {
expect(pageview).toBeDefined();
expect(typeof pageview).toBe('function');
});
it('should be callable', () => {
pageview('/test-page');
expect(pageview).toHaveBeenCalledWith('/test-page');
});
});
describe('event', () => {
it('should be defined', () => {
expect(event).toBeDefined();
expect(typeof event).toBe('function');
});
it('should be callable with all parameters', () => {
event('click', 'button', 'submit', 1);
expect(event).toHaveBeenCalledWith('click', 'button', 'submit', 1);
});
it('should be callable with minimal parameters', () => {
event('click', 'button');
expect(event).toHaveBeenCalledWith('click', 'button');
});
});
describe('trackContactForm', () => {
it('should be defined', () => {
expect(trackContactForm).toBeDefined();
expect(typeof trackContactForm).toBe('function');
});
it('should be callable', () => {
const formData = {
name: 'John Doe',
email: 'john@example.com',
message: 'Test message',
};
trackContactForm(formData);
expect(trackContactForm).toHaveBeenCalledWith(formData);
});
});
describe('trackButtonClick', () => {
it('should be defined', () => {
expect(trackButtonClick).toBeDefined();
expect(typeof trackButtonClick).toBe('function');
});
it('should be callable', () => {
trackButtonClick('submit', 'header');
expect(trackButtonClick).toHaveBeenCalledWith('submit', 'header');
});
});
describe('trackPageView', () => {
it('should be defined', () => {
expect(trackPageView).toBeDefined();
expect(typeof trackPageView).toBe('function');
});
it('should be callable', () => {
trackPageView('Home Page', '/home');
expect(trackPageView).toHaveBeenCalledWith('Home Page', '/home');
});
});
});
+37
View File
@@ -0,0 +1,37 @@
export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || '';
declare global {
interface Window {
gtag: (command: string, targetId: string, config?: Record<string, unknown>) => void;
}
}
export const pageview = (url: string) => {
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
window.gtag('config', GA_MEASUREMENT_ID, {
page_path: url,
});
}
};
export const event = (action: string, category: string, label?: string, value?: number) => {
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value,
});
}
};
export const trackContactForm = (formData: Record<string, string>) => {
event('submit', 'contact_form', 'contact_form_submission');
};
export const trackButtonClick = (buttonName: string, location: string) => {
event('click', 'button', `${location}_${buttonName}`);
};
export const trackPageView = (pageTitle: string, pagePath: string) => {
event('page_view', 'navigation', pageTitle);
};
+539
View File
@@ -0,0 +1,539 @@
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.transition as any;
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.transition as any;
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.transition as any;
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');
});
});
});
+106
View File
@@ -0,0 +1,106 @@
const mockInsert = jest.fn().mockReturnValue({
values: jest.fn().mockResolvedValue(undefined),
});
jest.mock('@/db', () => ({
db: {
insert: mockInsert,
},
}));
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'test-id'),
}));
import { createAuditLog, getActionLabel, getActionColor } from './audit';
describe('audit', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('createAuditLog', () => {
it('should create audit log successfully', async () => {
const mockValues = jest.fn().mockResolvedValue(undefined);
mockInsert.mockReturnValue({ values: mockValues });
const logData = {
userId: 'user-123',
action: 'LOGIN',
details: { ip: '192.168.1.1' },
};
await createAuditLog(logData);
expect(mockValues).toHaveBeenCalledWith(
expect.objectContaining({
id: 'test-id',
userId: 'user-123',
action: 'LOGIN',
details: { ip: '192.168.1.1' },
timestamp: expect.any(Date),
})
);
});
it('should handle missing optional fields', async () => {
const mockValues = jest.fn().mockResolvedValue(undefined);
mockInsert.mockReturnValue({ values: mockValues });
const logData = {
userId: 'user-456',
action: 'LOGOUT',
};
await createAuditLog(logData);
expect(mockValues).toHaveBeenCalledWith(
expect.objectContaining({
id: 'test-id',
userId: 'user-456',
action: 'LOGOUT',
timestamp: expect.any(Date),
})
);
});
it('should handle database errors gracefully', async () => {
const mockValues = jest.fn().mockRejectedValue(new Error('Database error'));
mockInsert.mockReturnValue({ values: mockValues });
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
await createAuditLog({
userId: 'user-789',
action: 'LOGIN',
});
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
describe('getActionLabel', () => {
it('should return correct label for known actions', () => {
expect(getActionLabel('login')).toBe('登录');
expect(getActionLabel('logout')).toBe('登出');
expect(getActionLabel('create')).toBe('创建');
expect(getActionLabel('update')).toBe('更新');
expect(getActionLabel('delete')).toBe('删除');
expect(getActionLabel('publish')).toBe('发布');
expect(getActionLabel('upload')).toBe('上传');
});
});
describe('getActionColor', () => {
it('should return correct color for known actions', () => {
expect(getActionColor('login')).toBe('bg-cyan-100 text-cyan-800');
expect(getActionColor('logout')).toBe('bg-gray-100 text-gray-800');
expect(getActionColor('create')).toBe('bg-green-100 text-green-800');
expect(getActionColor('update')).toBe('bg-blue-100 text-blue-800');
expect(getActionColor('delete')).toBe('bg-red-100 text-red-800');
expect(getActionColor('publish')).toBe('bg-purple-100 text-purple-800');
expect(getActionColor('upload')).toBe('bg-yellow-100 text-yellow-800');
});
});
});
+154
View File
@@ -0,0 +1,154 @@
import { describe, it, expect } from '@jest/globals';
jest.mock('next-auth', () => {
const mockNextAuth = jest.fn(() => ({
handlers: {
authOptions: {
providers: [
{
name: '邮箱密码',
credentials: {
email: { label: '邮箱', type: 'email' },
password: { label: '密码', type: 'password' },
},
},
],
callbacks: {
jwt: jest.fn(),
session: jest.fn(),
},
pages: {
signIn: '/admin/login',
error: '/admin/login',
},
session: {
strategy: 'jwt',
},
},
},
signIn: jest.fn(),
signOut: jest.fn(),
auth: jest.fn(),
}));
return {
__esModule: true,
default: mockNextAuth,
};
});
jest.mock('next-auth/providers/credentials', () => {
return jest.fn(() => ({
name: '邮箱密码',
credentials: {
email: { label: '邮箱', type: 'email' },
password: { label: '密码', type: 'password' },
},
}));
});
jest.mock('@/db', () => ({
db: {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn(),
},
}));
jest.mock('bcryptjs', () => ({
default: {
compare: jest.fn(),
},
}));
describe('Auth Module Configuration', () => {
describe('Provider Configuration', () => {
it('should export handlers', async () => {
const auth = await import('./auth');
expect(auth).toHaveProperty('handlers');
});
it('should export signIn function', async () => {
const auth = await import('./auth');
expect(auth).toHaveProperty('signIn');
expect(typeof auth.signIn).toBe('function');
});
it('should export signOut function', async () => {
const auth = await import('./auth');
expect(auth).toHaveProperty('signOut');
expect(typeof auth.signOut).toBe('function');
});
it('should export auth function', async () => {
const auth = await import('./auth');
expect(auth).toHaveProperty('auth');
expect(typeof auth.auth).toBe('function');
});
});
describe('Auth Options', () => {
it('should have authOptions in handlers', async () => {
const { handlers } = await import('./auth');
expect(handlers).toHaveProperty('authOptions');
});
it('should have providers configured', async () => {
const { handlers } = await import('./auth');
expect(handlers.authOptions).toHaveProperty('providers');
expect(Array.isArray(handlers.authOptions.providers)).toBe(true);
});
it('should have correct provider name', async () => {
const { handlers } = await import('./auth');
const provider = handlers.authOptions.providers[0];
expect(provider.name).toBe('邮箱密码');
});
it('should have email credential', async () => {
const { handlers } = await import('./auth');
const provider = handlers.authOptions.providers[0];
expect(provider.credentials).toHaveProperty('email');
});
it('should have password credential', async () => {
const { handlers } = await import('./auth');
const provider = handlers.authOptions.providers[0];
expect(provider.credentials).toHaveProperty('password');
});
});
describe('Page Configuration', () => {
it('should have correct sign-in page', async () => {
const { handlers } = await import('./auth');
expect(handlers.authOptions.pages.signIn).toBe('/admin/login');
});
it('should have correct error page', async () => {
const { handlers } = await import('./auth');
expect(handlers.authOptions.pages.error).toBe('/admin/login');
});
});
describe('Session Configuration', () => {
it('should use JWT session strategy', async () => {
const { handlers } = await import('./auth');
expect(handlers.authOptions.session.strategy).toBe('jwt');
});
});
describe('Callbacks', () => {
it('should have jwt callback', async () => {
const { handlers } = await import('./auth');
expect(handlers.authOptions.callbacks).toHaveProperty('jwt');
expect(typeof handlers.authOptions.callbacks.jwt).toBe('function');
});
it('should have session callback', async () => {
const { handlers } = await import('./auth');
expect(handlers.authOptions.callbacks).toHaveProperty('session');
expect(typeof handlers.authOptions.callbacks.session).toBe('function');
});
});
});
+165
View File
@@ -0,0 +1,165 @@
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { checkPermission, requirePermission } from './check-permission';
jest.mock('../auth', () => ({
auth: jest.fn(),
}));
import { auth } from '../auth';
const mockAuth = auth as jest.MockedFunction<typeof auth>;
describe('check-permission', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('checkPermission', () => {
it('should return allowed: false when no session', async () => {
mockAuth.mockResolvedValue(null as any);
const result = await checkPermission('content', 'read');
expect(result).toEqual({ allowed: false });
});
it('should return allowed: false when no user', async () => {
mockAuth.mockResolvedValue({} as any);
const result = await checkPermission('content', 'read');
expect(result).toEqual({ allowed: false });
});
it('should return allowed: true for admin with valid permission', async () => {
mockAuth.mockResolvedValue({
user: {
id: 'user-1',
role: 'admin',
},
} as any);
const result = await checkPermission('content', 'create');
expect(result.allowed).toBe(true);
expect(result.userId).toBe('user-1');
expect(result.role).toBe('admin');
});
it('should return allowed: false for viewer with invalid permission', async () => {
mockAuth.mockResolvedValue({
user: {
id: 'user-2',
role: 'viewer',
},
} as any);
const result = await checkPermission('content', 'create');
expect(result.allowed).toBe(false);
expect(result.userId).toBe('user-2');
expect(result.role).toBe('viewer');
});
it('should return allowed: true for editor with valid permission', async () => {
mockAuth.mockResolvedValue({
user: {
id: 'user-3',
role: 'editor',
},
} as any);
const result = await checkPermission('content', 'update');
expect(result.allowed).toBe(true);
expect(result.userId).toBe('user-3');
expect(result.role).toBe('editor');
});
it('should return allowed: false for editor with delete permission', async () => {
mockAuth.mockResolvedValue({
user: {
id: 'user-4',
role: 'editor',
},
} as any);
const result = await checkPermission('content', 'delete');
expect(result.allowed).toBe(false);
});
it('should handle different resources', async () => {
mockAuth.mockResolvedValue({
user: {
id: 'user-5',
role: 'admin',
},
} as any);
const result = await checkPermission('users', 'delete');
expect(result.allowed).toBe(true);
});
});
describe('requirePermission', () => {
it('should throw error when no permission', async () => {
mockAuth.mockResolvedValue({
user: {
id: 'user-6',
role: 'viewer',
},
} as any);
await expect(requirePermission('content', 'create')).rejects.toThrow('无权限执行此操作');
});
it('should return userId and role when has permission', async () => {
mockAuth.mockResolvedValue({
user: {
id: 'user-7',
role: 'admin',
},
} as any);
const result = await requirePermission('content', 'create');
expect(result).toEqual({
userId: 'user-7',
role: 'admin',
});
});
it('should throw error when no session', async () => {
mockAuth.mockResolvedValue(null as any);
await expect(requirePermission('content', 'read')).rejects.toThrow('无权限执行此操作');
});
it('should allow editor to publish content', async () => {
mockAuth.mockResolvedValue({
user: {
id: 'user-8',
role: 'editor',
},
} as any);
const result = await requirePermission('content', 'publish');
expect(result.userId).toBe('user-8');
expect(result.role).toBe('editor');
});
it('should deny viewer to update config', async () => {
mockAuth.mockResolvedValue({
user: {
id: 'user-9',
role: 'viewer',
},
} as any);
await expect(requirePermission('config', 'update')).rejects.toThrow('无权限执行此操作');
});
});
});
+127
View File
@@ -0,0 +1,127 @@
import {
calculateContrastRatio,
meetsWCAGStandard,
} from './color-contrast';
describe('color-contrast', () => {
describe('calculateContrastRatio', () => {
it('should calculate contrast ratio for black on white', () => {
const ratio = calculateContrastRatio('#000000', '#FFFFFF');
expect(ratio).toBeCloseTo(21, 0);
});
it('should calculate contrast ratio for white on black', () => {
const ratio = calculateContrastRatio('#FFFFFF', '#000000');
expect(ratio).toBeCloseTo(21, 0);
});
it('should calculate contrast ratio for gray on white', () => {
const ratio = calculateContrastRatio('#666666', '#FFFFFF');
expect(ratio).toBeGreaterThan(4);
expect(ratio).toBeLessThan(6);
});
it('should calculate contrast ratio for dark blue on white', () => {
const ratio = calculateContrastRatio('#00008B', '#FFFFFF');
expect(ratio).toBeGreaterThan(8);
});
it('should calculate contrast ratio for same colors', () => {
const ratio = calculateContrastRatio('#FF0000', '#FF0000');
expect(ratio).toBe(1);
});
it('should handle lowercase hex', () => {
const ratio1 = calculateContrastRatio('#000000', '#FFFFFF');
const ratio2 = calculateContrastRatio('#000000', '#ffffff');
expect(ratio1).toBe(ratio2);
});
});
describe('meetsWCAGStandard', () => {
describe('AA level - normal text', () => {
it('should pass for black on white', () => {
const result = meetsWCAGStandard('#000000', '#FFFFFF', 'AA', 'normal');
expect(result.passes).toBe(true);
expect(result.requiredRatio).toBe(4.5);
});
it('should fail for light gray on white', () => {
const result = meetsWCAGStandard('#CCCCCC', '#FFFFFF', 'AA', 'normal');
expect(result.passes).toBe(false);
expect(result.requiredRatio).toBe(4.5);
});
it('should pass for dark blue on white', () => {
const result = meetsWCAGStandard('#00008B', '#FFFFFF', 'AA', 'normal');
expect(result.passes).toBe(true);
});
});
describe('AA level - large text', () => {
it('should pass for gray on white', () => {
const result = meetsWCAGStandard('#666666', '#FFFFFF', 'AA', 'large');
expect(result.passes).toBe(true);
expect(result.requiredRatio).toBe(3);
});
it('should fail for light gray on white', () => {
const result = meetsWCAGStandard('#CCCCCC', '#FFFFFF', 'AA', 'large');
expect(result.passes).toBe(false);
expect(result.requiredRatio).toBe(3);
});
});
describe('AAA level - normal text', () => {
it('should pass for black on white', () => {
const result = meetsWCAGStandard('#000000', '#FFFFFF', 'AAA', 'normal');
expect(result.passes).toBe(true);
expect(result.requiredRatio).toBe(7);
});
it('should pass for dark blue on white', () => {
const result = meetsWCAGStandard('#00008B', '#FFFFFF', 'AAA', 'normal');
expect(result.passes).toBe(true);
expect(result.requiredRatio).toBe(7);
});
});
describe('AAA level - large text', () => {
it('should pass for dark blue on white', () => {
const result = meetsWCAGStandard('#00008B', '#FFFFFF', 'AAA', 'large');
expect(result.passes).toBe(true);
expect(result.requiredRatio).toBe(4.5);
});
it('should pass for gray on white', () => {
const result = meetsWCAGStandard('#666666', '#FFFFFF', 'AAA', 'large');
expect(result.passes).toBe(true);
expect(result.requiredRatio).toBe(4.5);
});
});
describe('default parameters', () => {
it('should default to AA level', () => {
const result = meetsWCAGStandard('#000000', '#FFFFFF');
expect(result.requiredRatio).toBe(4.5);
});
it('should default to normal text size', () => {
const result = meetsWCAGStandard('#000000', '#FFFFFF', 'AA');
expect(result.requiredRatio).toBe(4.5);
});
});
describe('result structure', () => {
it('should return result with all required properties', () => {
const result = meetsWCAGStandard('#000000', '#FFFFFF');
expect(result).toHaveProperty('passes');
expect(result).toHaveProperty('ratio');
expect(result).toHaveProperty('requiredRatio');
expect(typeof result.passes).toBe('boolean');
expect(typeof result.ratio).toBe('number');
expect(typeof result.requiredRatio).toBe('number');
});
});
});
});
+183
View File
@@ -0,0 +1,183 @@
import { describe, it, expect } from '@jest/globals';
import { brandColors, colorValues, gradients } from './colors';
describe('colors', () => {
describe('brandColors', () => {
it('should have primary color palette', () => {
expect(brandColors.primary).toBeDefined();
expect(brandColors.primary[900]).toBe('#0A0A0A');
expect(brandColors.primary[50]).toBe('#F5F5F5');
});
it('should have brand color palette', () => {
expect(brandColors.brand).toBeDefined();
expect(brandColors.brand[600]).toBe('#C41E3A');
expect(brandColors.brand[100]).toBe('#FEF2F4');
});
it('should have neutral color palette', () => {
expect(brandColors.neutral).toBeDefined();
expect(brandColors.neutral[900]).toBe('#1C1C1C');
expect(brandColors.neutral[50]).toBe('#FFFBF5');
});
it('should have success color palette', () => {
expect(brandColors.success).toBeDefined();
expect(brandColors.success[600]).toBe('#16A34A');
expect(brandColors.success[100]).toBe('#F0FDF4');
});
it('should have warning color palette', () => {
expect(brandColors.warning).toBeDefined();
expect(brandColors.warning[600]).toBe('#D97706');
expect(brandColors.warning[100]).toBe('#FFFBEB');
});
it('should have info color palette', () => {
expect(brandColors.info).toBeDefined();
expect(brandColors.info[600]).toBe('#5C5C5C');
expect(brandColors.info[100]).toBe('#F5F5F5');
});
it('should have error color palette', () => {
expect(brandColors.error).toBeDefined();
expect(brandColors.error[600]).toBe('#C41E3A');
expect(brandColors.error[100]).toBe('#FEF2F4');
});
it('should have valid hex color format', () => {
const hexPattern = /^#[0-9A-Fa-f]{6}$/;
Object.values(brandColors.primary).forEach(color => {
expect(color).toMatch(hexPattern);
});
Object.values(brandColors.brand).forEach(color => {
expect(color).toMatch(hexPattern);
});
});
});
describe('colorValues', () => {
it('should have primary colors', () => {
expect(colorValues.primary).toBe('#1C1C1C');
expect(colorValues.primaryHover).toBe('#0A0A0A');
expect(colorValues.primaryLight).toBe('#3D3D3D');
expect(colorValues.primaryLighter).toBe('#F5F5F5');
});
it('should have brand colors', () => {
expect(colorValues.brand).toBe('#C41E3A');
expect(colorValues.brandHover).toBe('#A01830');
expect(colorValues.brandLight).toBe('#E04A68');
expect(colorValues.brandBg).toBe('#FEF2F4');
});
it('should have text colors', () => {
expect(colorValues.textPrimary).toBe('#1C1C1C');
expect(colorValues.textSecondary).toBe('#3D3D3D');
expect(colorValues.textTertiary).toBe('#5C5C5C');
expect(colorValues.textMuted).toBe('#8C8C8C');
});
it('should have background colors', () => {
expect(colorValues.bgPrimary).toBe('#FAFAFA');
expect(colorValues.bgSecondary).toBe('#FFFBF5');
expect(colorValues.bgTertiary).toBe('#F5F5F5');
expect(colorValues.bgHover).toBe('#EFEFEF');
});
it('should have border colors', () => {
expect(colorValues.border).toBe('#E5E5E5');
expect(colorValues.borderSecondary).toBe('#D4D4D4');
expect(colorValues.borderAccent).toBe('#1C1C1C');
});
it('should have link colors', () => {
expect(colorValues.link).toBe('#1C1C1C');
expect(colorValues.linkHover).toBe('#C41E3A');
});
it('should have status colors', () => {
expect(colorValues.success).toBe('#16A34A');
expect(colorValues.successBg).toBe('#F0FDF4');
expect(colorValues.warning).toBe('#D97706');
expect(colorValues.warningBg).toBe('#FFFBEB');
expect(colorValues.info).toBe('#5C5C5C');
expect(colorValues.infoBg).toBe('#F5F5F5');
expect(colorValues.error).toBe('#C41E3A');
expect(colorValues.errorBg).toBe('#FEF2F4');
});
it('should have valid hex color format', () => {
const hexPattern = /^#[0-9A-Fa-f]{6}$/;
Object.values(colorValues).forEach(color => {
expect(color).toMatch(hexPattern);
});
});
});
describe('gradients', () => {
it('should have primary gradient', () => {
expect(gradients.primary).toBeDefined();
expect(gradients.primary).toContain('linear-gradient');
expect(gradients.primary).toContain('135deg');
});
it('should have hero gradient', () => {
expect(gradients.hero).toBeDefined();
expect(gradients.hero).toContain('linear-gradient');
expect(gradients.hero).toContain('180deg');
});
it('should have brand gradient', () => {
expect(gradients.brand).toBeDefined();
expect(gradients.brand).toContain('linear-gradient');
expect(gradients.brand).toContain('135deg');
});
it('should have subtle gradient', () => {
expect(gradients.subtle).toBeDefined();
expect(gradients.subtle).toContain('linear-gradient');
expect(gradients.subtle).toContain('180deg');
});
it('should have card gradient', () => {
expect(gradients.card).toBeDefined();
expect(gradients.card).toContain('linear-gradient');
expect(gradients.card).toContain('180deg');
});
it('should have cta gradient', () => {
expect(gradients.cta).toBeDefined();
expect(gradients.cta).toContain('linear-gradient');
expect(gradients.cta).toContain('135deg');
});
it('should have valid gradient format', () => {
const gradientPattern = /^linear-gradient\(/;
Object.values(gradients).forEach(gradient => {
expect(gradient).toMatch(gradientPattern);
});
});
});
describe('Type Exports', () => {
it('should export BrandColor type', () => {
const color: typeof brandColors = brandColors;
expect(color).toBeDefined();
});
it('should export ColorValue type', () => {
const value: typeof colorValues = colorValues;
expect(value).toBeDefined();
});
it('should export Gradient type', () => {
const gradient: typeof gradients = gradients;
expect(gradient).toBeDefined();
});
});
});
+242
View File
@@ -0,0 +1,242 @@
import { describe, it, expect } from '@jest/globals';
import {
COMPANY_INFO,
NAVIGATION,
STATS,
SERVICES,
PRODUCTS,
NEWS,
} from './constants';
describe('Constants', () => {
describe('COMPANY_INFO', () => {
it('should have company name', () => {
expect(COMPANY_INFO.name).toBe('四川睿新致远科技有限公司');
});
it('should have short name', () => {
expect(COMPANY_INFO.shortName).toBe('睿新致遠');
});
it('should have slogan', () => {
expect(COMPANY_INFO.slogan).toBe('智连未来,成长伙伴');
});
it('should have contact information', () => {
expect(COMPANY_INFO.email).toBeDefined();
expect(COMPANY_INFO.phone).toBeDefined();
expect(COMPANY_INFO.address).toBeDefined();
});
it('should have legal information', () => {
expect(COMPANY_INFO.icp).toBeDefined();
expect(COMPANY_INFO.police).toBeDefined();
});
});
describe('NAVIGATION', () => {
it('should be an array', () => {
expect(Array.isArray(NAVIGATION)).toBe(true);
});
it('should have navigation items', () => {
expect(NAVIGATION.length).toBeGreaterThan(0);
});
it('should have required properties', () => {
NAVIGATION.forEach(item => {
expect(item).toHaveProperty('id');
expect(item).toHaveProperty('label');
expect(item).toHaveProperty('href');
});
});
it('should have home navigation', () => {
const homeNav = NAVIGATION.find(item => item.id === 'home');
expect(homeNav).toBeDefined();
expect(homeNav?.label).toBe('首页');
});
it('should have contact navigation', () => {
const contactNav = NAVIGATION.find(item => item.id === 'contact');
expect(contactNav).toBeDefined();
expect(contactNav?.href).toBe('/contact');
});
});
describe('STATS', () => {
it('should be an array', () => {
expect(Array.isArray(STATS)).toBe(true);
});
it('should have stat items', () => {
expect(STATS.length).toBeGreaterThan(0);
});
it('should have required properties', () => {
STATS.forEach(stat => {
expect(stat).toHaveProperty('value');
expect(stat).toHaveProperty('label');
});
});
it('should have numeric values', () => {
STATS.forEach(stat => {
expect(stat.value).toMatch(/\d+/);
});
});
});
describe('SERVICES', () => {
it('should be an array', () => {
expect(Array.isArray(SERVICES)).toBe(true);
});
it('should have service items', () => {
expect(SERVICES.length).toBeGreaterThan(0);
});
it('should have required properties', () => {
SERVICES.forEach(service => {
expect(service).toHaveProperty('id');
expect(service).toHaveProperty('title');
expect(service).toHaveProperty('description');
expect(service).toHaveProperty('icon');
expect(service).toHaveProperty('features');
expect(service).toHaveProperty('benefits');
expect(service).toHaveProperty('process');
});
});
it('should have software service', () => {
const softwareService = SERVICES.find(s => s.id === 'software');
expect(softwareService).toBeDefined();
expect(softwareService?.title).toBe('软件开发');
});
it('should have cloud service', () => {
const cloudService = SERVICES.find(s => s.id === 'cloud');
expect(cloudService).toBeDefined();
expect(cloudService?.title).toBe('云服务');
});
it('should have data service', () => {
const dataService = SERVICES.find(s => s.id === 'data');
expect(dataService).toBeDefined();
expect(dataService?.title).toBe('数据分析');
});
it('should have security service', () => {
const securityService = SERVICES.find(s => s.id === 'security');
expect(securityService).toBeDefined();
expect(securityService?.title).toBe('信息安全');
});
it('should have features as array', () => {
SERVICES.forEach(service => {
expect(Array.isArray(service.features)).toBe(true);
expect(service.features.length).toBeGreaterThan(0);
});
});
it('should have benefits as array', () => {
SERVICES.forEach(service => {
expect(Array.isArray(service.benefits)).toBe(true);
expect(service.benefits.length).toBeGreaterThan(0);
});
});
});
describe('PRODUCTS', () => {
it('should be an array', () => {
expect(Array.isArray(PRODUCTS)).toBe(true);
});
it('should have product items', () => {
expect(PRODUCTS.length).toBeGreaterThan(0);
});
it('should have required properties', () => {
PRODUCTS.forEach(product => {
expect(product).toHaveProperty('id');
expect(product).toHaveProperty('title');
expect(product).toHaveProperty('description');
expect(product).toHaveProperty('category');
expect(product).toHaveProperty('features');
expect(product).toHaveProperty('benefits');
expect(product).toHaveProperty('pricing');
});
});
it('should have ERP product', () => {
const erpProduct = PRODUCTS.find(p => p.id === 'erp');
expect(erpProduct).toBeDefined();
expect(erpProduct?.title).toContain('ERP');
});
it('should have CRM product', () => {
const crmProduct = PRODUCTS.find(p => p.id === 'crm');
expect(crmProduct).toBeDefined();
expect(crmProduct?.title).toContain('客户关系管理');
});
it('should have CMS product', () => {
const cmsProduct = PRODUCTS.find(p => p.id === 'cms');
expect(cmsProduct).toBeDefined();
expect(cmsProduct?.title).toContain('内容管理');
});
it('should have BI product', () => {
const biProduct = PRODUCTS.find(p => p.id === 'bi');
expect(biProduct).toBeDefined();
expect(biProduct?.title).toContain('商业智能');
});
it('should have pricing object', () => {
PRODUCTS.forEach(product => {
expect(product.pricing).toHaveProperty('base');
expect(product.pricing).toHaveProperty('standard');
expect(product.pricing).toHaveProperty('enterprise');
});
});
});
describe('NEWS', () => {
it('should be an array', () => {
expect(Array.isArray(NEWS)).toBe(true);
});
it('should have news items', () => {
expect(NEWS.length).toBeGreaterThan(0);
});
it('should have required properties', () => {
NEWS.forEach(news => {
expect(news).toHaveProperty('id');
expect(news).toHaveProperty('title');
expect(news).toHaveProperty('excerpt');
expect(news).toHaveProperty('date');
expect(news).toHaveProperty('category');
expect(news).toHaveProperty('content');
});
});
it('should have valid categories', () => {
const validCategories = ['公司新闻', '产品发布', '合作动态', '行业资讯'];
NEWS.forEach(news => {
expect(validCategories).toContain(news.category);
});
});
it('should have valid date format', () => {
NEWS.forEach(news => {
expect(news.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
});
it('should have founding news', () => {
const foundingNews = NEWS.find(n => n.title.includes('成立'));
expect(foundingNews).toBeDefined();
});
});
});
+66
View File
@@ -0,0 +1,66 @@
import {
generateCSRFToken,
validateCSRFToken,
getCSRFTokenFromStorage,
setCSRFTokenToStorage,
} from './csrf';
describe('csrf', () => {
describe('generateCSRFToken', () => {
it('should generate a token of correct length', () => {
const token = generateCSRFToken();
expect(token).toHaveLength(64);
});
it('should generate unique tokens', () => {
const token1 = generateCSRFToken();
const token2 = generateCSRFToken();
expect(token1).not.toBe(token2);
});
it('should only contain hexadecimal characters', () => {
const token = generateCSRFToken();
expect(token).toMatch(/^[0-9a-f]{64}$/);
});
});
describe('validateCSRFToken', () => {
it('should return true for matching tokens', () => {
const token = generateCSRFToken();
expect(validateCSRFToken(token, token)).toBe(true);
});
it('should return false for mismatched tokens', () => {
const token1 = generateCSRFToken();
const token2 = generateCSRFToken();
expect(validateCSRFToken(token1, token2)).toBe(false);
});
it('should return false for empty tokens', () => {
expect(validateCSRFToken('', '')).toBe(false);
expect(validateCSRFToken('token', '')).toBe(false);
expect(validateCSRFToken('', 'token')).toBe(false);
});
});
describe('getCSRFTokenFromStorage', () => {
it('should return token from sessionStorage', () => {
sessionStorage.setItem('csrf_token', 'test-token');
const token = getCSRFTokenFromStorage();
expect(token).toBe('test-token');
});
it('should return null when token not found', () => {
sessionStorage.removeItem('csrf_token');
const token = getCSRFTokenFromStorage();
expect(token).toBeNull();
});
});
describe('setCSRFTokenToStorage', () => {
it('should set token in sessionStorage', () => {
setCSRFTokenToStorage('test-token');
expect(sessionStorage.getItem('csrf_token')).toBe('test-token');
});
});
});
+379
View File
@@ -0,0 +1,379 @@
import { describe, it, expect, beforeEach } from '@jest/globals';
jest.mock('./constants', () => ({
COMPANY_INFO: {
name: '诺瓦隆科技',
email: 'contact@novalon.cn',
phone: '400-123-4567',
address: '北京市朝阳区科技园区',
},
}));
describe('Email Templates', () => {
const mockContactData = {
name: '张三',
phone: '13800138000',
email: 'zhangsan@example.com',
message: '这是一条测试留言',
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('generateNotificationEmail', () => {
it('should generate valid HTML email', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('<!DOCTYPE html>');
expect(email).toContain('<html>');
expect(email).toContain('</html>');
});
it('should include customer name', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('张三');
expect(email).toContain('客户姓名');
});
it('should include customer phone', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('13800138000');
expect(email).toContain('联系电话');
});
it('should include customer email', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('zhangsan@example.com');
expect(email).toContain('电子邮箱');
});
it('should include message content', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('这是一条测试留言');
expect(email).toContain('留言内容');
});
it('should include company name', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('诺瓦隆科技');
});
it('should include submit time', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('提交时间');
});
it('should include mailto link', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('mailto:zhangsan@example.com');
expect(email).toContain('快速回复');
});
it('should include company address in footer', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('北京市朝阳区科技园区');
});
it('should have proper email title', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('官网留言通知');
});
it('should include responsive meta tag', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('viewport');
expect(email).toContain('width=device-width');
});
it('should include UTF-8 charset', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('charset="utf-8"');
});
it('should handle long messages', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const longMessage = '这是一条很长的留言'.repeat(100);
const data = { ...mockContactData, message: longMessage };
const email = generateNotificationEmail(data);
expect(email).toContain(longMessage);
});
it('should handle special characters in name', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const data = { ...mockContactData, name: '张三 <script>alert("xss")</script>' };
const email = generateNotificationEmail(data);
expect(email).toContain('张三 <script>alert("xss")</script>');
});
it('should handle special characters in message', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const data = { ...mockContactData, message: '测试 & < > " \'' };
const email = generateNotificationEmail(data);
expect(email).toContain('测试 & < > " \'');
});
});
describe('generateConfirmationEmail', () => {
it('should generate valid HTML email', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('<!DOCTYPE html>');
expect(email).toContain('<html>');
expect(email).toContain('</html>');
});
it('should include customer name', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('张三');
expect(email).toContain('尊敬的');
});
it('should include message content', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('这是一条测试留言');
expect(email).toContain('您的留言内容');
});
it('should include company name', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('诺瓦隆科技');
});
it('should include company contact information', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('contact@novalon.cn');
expect(email).toContain('400-123-4567');
expect(email).toContain('北京市朝阳区科技园区');
});
it('should include expected response time', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('预计回复时间');
expect(email).toContain('2小时内');
});
it('should include working hours', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('工作日');
expect(email).toContain('9:00 - 18:00');
});
it('should have proper email title', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('感谢您的留言');
});
it('should include success icon', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('🎉');
});
it('should include current year in footer', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
const currentYear = new Date().getFullYear().toString();
expect(email).toContain(`© ${currentYear}`);
});
it('should include responsive meta tag', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('viewport');
expect(email).toContain('width=device-width');
});
it('should include UTF-8 charset', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('charset="utf-8"');
});
it('should handle long messages', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const longMessage = '这是一条很长的留言'.repeat(100);
const data = { ...mockContactData, message: longMessage };
const email = generateConfirmationEmail(data);
expect(email).toContain(longMessage);
});
it('should handle special characters in name', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const data = { ...mockContactData, name: '张三 <script>alert("xss")</script>' };
const email = generateConfirmationEmail(data);
expect(email).toContain('张三 <script>alert("xss")</script>');
});
});
describe('Email Template Structure', () => {
it('should have consistent styling in notification email', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('style');
expect(email).toContain('font-family');
expect(email).toContain('max-width: 600px');
});
it('should have consistent styling in confirmation email', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('style');
expect(email).toContain('font-family');
expect(email).toContain('max-width: 600px');
});
it('should use brand colors in notification email', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('#C41E3A');
expect(email).toContain('#1C1C1C');
});
it('should use brand colors in confirmation email', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('#C41E3A');
expect(email).toContain('#1C1C1C');
});
it('should have proper container structure in notification email', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const email = generateNotificationEmail(mockContactData);
expect(email).toContain('class="container"');
expect(email).toContain('class="header"');
expect(email).toContain('class="content"');
expect(email).toContain('class="footer"');
});
it('should have proper container structure in confirmation email', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const email = generateConfirmationEmail(mockContactData);
expect(email).toContain('class="container"');
expect(email).toContain('class="header"');
expect(email).toContain('class="content"');
expect(email).toContain('class="footer"');
});
});
describe('Edge Cases', () => {
it('should handle empty message', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const data = { ...mockContactData, message: '' };
const email = generateNotificationEmail(data);
expect(email).toContain('留言内容');
});
it('should handle empty name', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const data = { ...mockContactData, name: '' };
const email = generateConfirmationEmail(data);
expect(email).toContain('尊敬的');
});
it('should handle email with special characters', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const data = { ...mockContactData, email: 'test+special@example.com' };
const email = generateNotificationEmail(data);
expect(email).toContain('test+special@example.com');
});
it('should handle phone number with spaces', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const data = { ...mockContactData, phone: '138 0013 8000' };
const email = generateNotificationEmail(data);
expect(email).toContain('138 0013 8000');
});
it('should handle unicode characters in message', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const data = { ...mockContactData, message: '测试 emoji: 😀 🎉 📧' };
const email = generateNotificationEmail(data);
expect(email).toContain('测试 emoji: 😀 🎉 📧');
});
});
describe('Performance Tests', () => {
it('should generate notification email quickly', async () => {
const { generateNotificationEmail } = await import('./email-templates');
const start = performance.now();
for (let i = 0; i < 100; i++) {
generateNotificationEmail(mockContactData);
}
const end = performance.now();
expect(end - start).toBeLessThan(1000);
});
it('should generate confirmation email quickly', async () => {
const { generateConfirmationEmail } = await import('./email-templates');
const start = performance.now();
for (let i = 0; i < 100; i++) {
generateConfirmationEmail(mockContactData);
}
const end = performance.now();
expect(end - start).toBeLessThan(1000);
});
});
});
+132
View File
@@ -0,0 +1,132 @@
import { describe, it, expect } from '@jest/globals';
import {
getGradientStyle,
getGlowStyle,
getBorderGradientStyle,
getTextGradientStyle,
getHeroGradientStyle,
getCTAGradientStyle,
} from './gradients';
describe('gradients', () => {
describe('getGradientStyle', () => {
it('should return primary gradient style', () => {
const style = getGradientStyle('primary');
expect(style).toHaveProperty('background');
expect(style.background).toContain('linear-gradient');
});
it('should return hero gradient style', () => {
const style = getGradientStyle('hero');
expect(style).toHaveProperty('background');
expect(style.background).toContain('linear-gradient');
});
it('should return brand gradient style', () => {
const style = getGradientStyle('brand');
expect(style).toHaveProperty('background');
expect(style.background).toContain('linear-gradient');
});
it('should return subtle gradient style', () => {
const style = getGradientStyle('subtle');
expect(style).toHaveProperty('background');
expect(style.background).toContain('linear-gradient');
});
it('should return card gradient style', () => {
const style = getGradientStyle('card');
expect(style).toHaveProperty('background');
expect(style.background).toContain('linear-gradient');
});
it('should return cta gradient style', () => {
const style = getGradientStyle('cta');
expect(style).toHaveProperty('background');
expect(style.background).toContain('linear-gradient');
});
});
describe('getGlowStyle', () => {
it('should return primary glow style with default opacity', () => {
const style = getGlowStyle('primary');
expect(style).toHaveProperty('background');
expect(style.background).toContain('radial-gradient');
expect(style.background).toContain('0, 94, 184');
});
it('should return brand glow style with default opacity', () => {
const style = getGlowStyle('brand');
expect(style).toHaveProperty('background');
expect(style.background).toContain('radial-gradient');
expect(style.background).toContain('196, 30, 58');
});
it('should return glow style with custom opacity', () => {
const style = getGlowStyle('primary', 0.5);
expect(style).toHaveProperty('background');
expect(style.background).toContain('0.5');
});
it('should return glow style with zero opacity', () => {
const style = getGlowStyle('brand', 0);
expect(style).toHaveProperty('background');
expect(style.background).toContain('0)');
});
it('should return glow style with full opacity', () => {
const style = getGlowStyle('primary', 1);
expect(style).toHaveProperty('background');
expect(style.background).toContain('1)');
});
});
describe('getBorderGradientStyle', () => {
it('should return border gradient style', () => {
const style = getBorderGradientStyle();
expect(style).toHaveProperty('borderImage');
expect(style.borderImage).toContain('linear-gradient');
});
});
describe('getTextGradientStyle', () => {
it('should return primary text gradient style', () => {
const style = getTextGradientStyle('primary');
expect(style).toHaveProperty('background');
expect(style).toHaveProperty('WebkitBackgroundClip');
expect(style).toHaveProperty('WebkitTextFillColor');
expect(style).toHaveProperty('backgroundClip');
expect(style.WebkitBackgroundClip).toBe('text');
expect(style.WebkitTextFillColor).toBe('transparent');
});
it('should return brand text gradient style', () => {
const style = getTextGradientStyle('brand');
expect(style).toHaveProperty('background');
expect(style.background).toContain('linear-gradient');
});
it('should default to primary gradient', () => {
const style = getTextGradientStyle();
expect(style).toHaveProperty('background');
});
});
describe('getHeroGradientStyle', () => {
it('should return hero gradient style', () => {
const style = getHeroGradientStyle();
expect(style).toHaveProperty('background');
expect(style.background).toContain('linear-gradient');
expect(style.background).toContain('180deg');
});
});
describe('getCTAGradientStyle', () => {
it('should return CTA gradient style', () => {
const style = getCTAGradientStyle();
expect(style).toHaveProperty('background');
expect(style.background).toContain('linear-gradient');
expect(style.background).toContain('135deg');
});
});
});
+169
View File
@@ -0,0 +1,169 @@
import { sanitizeInput, sanitizeHTML, sanitizeURL } from './sanitize';
import { isAllowedType, validateFileSignature, isDangerousFile } from './upload';
import { generateCSRFToken, validateCSRFToken } from './csrf';
import { calculateContrastRatio, meetsWCAGStandard } from './color-contrast';
import { PerformanceMonitor } from './monitoring';
describe('Integration Tests', () => {
describe('Input Sanitization Flow', () => {
it('should sanitize user input end-to-end', () => {
const userInput = '<script>alert("xss")</script>Hello World';
const sanitized = sanitizeInput(userInput);
expect(sanitized).not.toContain('<script>');
expect(sanitized).toContain('Hello World');
});
it('should sanitize HTML content', () => {
const htmlContent = '<p onclick="alert(1)">Safe text</p><script>evil()</script>';
const sanitized = sanitizeHTML(htmlContent);
expect(sanitized).not.toContain('onclick');
expect(sanitized).not.toContain('<script>');
expect(sanitized).toContain('Safe text');
});
it('should sanitize URLs', () => {
const maliciousUrl = 'javascript:alert(1)';
const safeUrl = 'https://example.com';
expect(sanitizeURL(maliciousUrl)).toBe('');
expect(sanitizeURL(safeUrl)).toBe(safeUrl);
});
});
describe('CSRF Protection Flow', () => {
it('should generate and validate CSRF tokens', () => {
const token = generateCSRFToken();
expect(token).toBeDefined();
expect(typeof token).toBe('string');
expect(token.length).toBeGreaterThan(0);
const isValid = validateCSRFToken(token, token);
expect(isValid).toBe(true);
});
it('should reject invalid CSRF tokens', () => {
const invalidToken = 'invalid-token-123';
const storedToken = 'different-token';
const isValid = validateCSRFToken(invalidToken, storedToken);
expect(isValid).toBe(false);
});
it('should reject empty tokens', () => {
const isValid = validateCSRFToken('', 'some-token');
expect(isValid).toBe(false);
});
});
describe('File Validation Flow', () => {
it('should validate allowed file types', () => {
const isImageAllowed = isAllowedType('image/jpeg', 'image');
const isDocAllowed = isAllowedType('application/pdf', 'document');
expect(isImageAllowed).toBe(true);
expect(isDocAllowed).toBe(true);
});
it('should reject dangerous file types', () => {
const isDangerous = isDangerousFile('malware.exe');
expect(isDangerous).toBe(true);
const isSafe = isDangerousFile('document.pdf');
expect(isSafe).toBe(false);
});
it('should validate file signatures', async () => {
const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const isValid = validateFileSignature(pngSignature, 'image/png');
expect(isValid).toBe(true);
});
});
describe('Color Contrast Validation Flow', () => {
it('should validate accessible color combinations', () => {
const result = meetsWCAGStandard('#000000', '#FFFFFF', 'AA', 'normal');
expect(result.passes).toBe(true);
expect(result.ratio).toBeGreaterThan(4.5);
});
it('should reject inaccessible color combinations', () => {
const result = meetsWCAGStandard('#CCCCCC', '#FFFFFF', 'AA', 'normal');
expect(result.passes).toBe(false);
});
it('should calculate contrast ratios correctly', () => {
const blackOnWhite = calculateContrastRatio('#000000', '#FFFFFF');
const whiteOnBlack = calculateContrastRatio('#FFFFFF', '#000000');
expect(blackOnWhite).toBeCloseTo(21, 0);
expect(whiteOnBlack).toBeCloseTo(21, 0);
});
});
describe('Performance Monitoring Flow', () => {
beforeEach(() => {
PerformanceMonitor['instance'] = null as any;
});
it('should track metrics across operations', () => {
const monitor = PerformanceMonitor.getInstance();
monitor.recordMetric('response_time', 100);
monitor.recordMetric('response_time', 200);
monitor.recordMetric('response_time', 150);
const stats = monitor.getStats('response_time');
expect(stats.count).toBe(3);
expect(stats.avg).toBe(150);
expect(stats.min).toBe(100);
expect(stats.max).toBe(200);
});
it('should track multiple metrics independently', () => {
const monitor = PerformanceMonitor.getInstance();
monitor.recordMetric('api_calls', 1);
monitor.recordMetric('api_calls', 1);
monitor.recordMetric('db_queries', 5);
monitor.recordMetric('db_queries', 5);
expect(monitor.getCount('api_calls')).toBe(2);
expect(monitor.getCount('db_queries')).toBe(2);
expect(monitor.getAverage('api_calls')).toBe(1);
expect(monitor.getAverage('db_queries')).toBe(5);
});
});
describe('Combined Security Validation Flow', () => {
it('should validate and sanitize user input comprehensively', () => {
const maliciousInput = {
name: '<script>alert("xss")</script>John',
email: 'john@example.com',
website: 'javascript:alert(1)',
message: '<p onclick="evil()">Hello</p>',
};
const sanitized = {
name: sanitizeInput(maliciousInput.name),
email: sanitizeInput(maliciousInput.email),
website: sanitizeURL(maliciousInput.website),
message: sanitizeHTML(maliciousInput.message),
};
expect(sanitized.name).not.toContain('<script>');
expect(sanitized.email).toBe('john@example.com');
expect(sanitized.website).toBe('');
expect(sanitized.message).not.toContain('onclick');
});
it('should validate file upload with CSRF protection', () => {
const csrfToken = generateCSRFToken();
const isValidCSRF = validateCSRFToken(csrfToken, csrfToken);
expect(isValidCSRF).toBe(true);
const isAllowed = isAllowedType('application/pdf', 'document');
expect(isAllowed).toBe(true);
});
});
});
+200
View File
@@ -0,0 +1,200 @@
import { PerformanceMonitor, monitor } from './monitoring';
describe('PerformanceMonitor', () => {
beforeEach(() => {
PerformanceMonitor['instance'] = null as any;
});
describe('getInstance', () => {
it('should return singleton instance', () => {
const instance1 = PerformanceMonitor.getInstance();
const instance2 = PerformanceMonitor.getInstance();
expect(instance1).toBe(instance2);
});
});
describe('recordMetric', () => {
it('should record a metric value', () => {
const instance = PerformanceMonitor.getInstance();
instance.recordMetric('test-metric', 100);
expect(instance.getCount('test-metric')).toBe(1);
});
it('should record multiple metric values', () => {
const instance = PerformanceMonitor.getInstance();
instance.recordMetric('test-metric', 100);
instance.recordMetric('test-metric', 200);
instance.recordMetric('test-metric', 300);
expect(instance.getCount('test-metric')).toBe(3);
});
it('should maintain max 1000 values per metric', () => {
const instance = PerformanceMonitor.getInstance();
for (let i = 0; i < 1005; i++) {
instance.recordMetric('test-metric', i);
}
expect(instance.getCount('test-metric')).toBe(1000);
});
});
describe('getAverage', () => {
it('should return 0 for empty metric', () => {
const instance = PerformanceMonitor.getInstance();
expect(instance.getAverage('empty-metric')).toBe(0);
});
it('should calculate average correctly', () => {
const instance = PerformanceMonitor.getInstance();
instance.recordMetric('test-metric', 100);
instance.recordMetric('test-metric', 200);
instance.recordMetric('test-metric', 300);
expect(instance.getAverage('test-metric')).toBe(200);
});
});
describe('getPercentile', () => {
it('should return 0 for empty metric', () => {
const instance = PerformanceMonitor.getInstance();
expect(instance.getPercentile('empty-metric', 50)).toBe(0);
});
it('should calculate 50th percentile correctly', () => {
const instance = PerformanceMonitor.getInstance();
for (let i = 1; i <= 100; i++) {
instance.recordMetric('test-metric', i);
}
expect(instance.getPercentile('test-metric', 50)).toBe(50);
});
it('should calculate 95th percentile correctly', () => {
const instance = PerformanceMonitor.getInstance();
for (let i = 1; i <= 100; i++) {
instance.recordMetric('test-metric', i);
}
expect(instance.getPercentile('test-metric', 95)).toBe(95);
});
it('should calculate 99th percentile correctly', () => {
const instance = PerformanceMonitor.getInstance();
for (let i = 1; i <= 100; i++) {
instance.recordMetric('test-metric', i);
}
expect(instance.getPercentile('test-metric', 99)).toBe(99);
});
});
describe('getCount', () => {
it('should return 0 for non-existent metric', () => {
const instance = PerformanceMonitor.getInstance();
expect(instance.getCount('non-existent')).toBe(0);
});
it('should return count for existing metric', () => {
const instance = PerformanceMonitor.getInstance();
instance.recordMetric('test-metric', 100);
instance.recordMetric('test-metric', 200);
expect(instance.getCount('test-metric')).toBe(2);
});
});
describe('getMin', () => {
it('should return 0 for empty metric', () => {
const instance = PerformanceMonitor.getInstance();
expect(instance.getMin('empty-metric')).toBe(0);
});
it('should return minimum value', () => {
const instance = PerformanceMonitor.getInstance();
instance.recordMetric('test-metric', 100);
instance.recordMetric('test-metric', 50);
instance.recordMetric('test-metric', 200);
expect(instance.getMin('test-metric')).toBe(50);
});
});
describe('getMax', () => {
it('should return 0 for empty metric', () => {
const instance = PerformanceMonitor.getInstance();
expect(instance.getMax('empty-metric')).toBe(0);
});
it('should return maximum value', () => {
const instance = PerformanceMonitor.getInstance();
instance.recordMetric('test-metric', 100);
instance.recordMetric('test-metric', 200);
instance.recordMetric('test-metric', 50);
expect(instance.getMax('test-metric')).toBe(200);
});
});
describe('getStats', () => {
it('should return stats for empty metric', () => {
const instance = PerformanceMonitor.getInstance();
const stats = instance.getStats('empty-metric');
expect(stats).toEqual({
count: 0,
avg: 0,
min: 0,
max: 0,
p50: 0,
p95: 0,
p99: 0,
});
});
it('should return complete stats for metric', () => {
const instance = PerformanceMonitor.getInstance();
for (let i = 1; i <= 100; i++) {
instance.recordMetric('test-metric', i);
}
const stats = instance.getStats('test-metric');
expect(stats.count).toBe(100);
expect(stats.avg).toBe(50.5);
expect(stats.min).toBe(1);
expect(stats.max).toBe(100);
expect(stats.p50).toBe(50);
expect(stats.p95).toBe(95);
expect(stats.p99).toBe(99);
});
});
describe('clearMetrics', () => {
it('should clear specific metric', () => {
const instance = PerformanceMonitor.getInstance();
instance.recordMetric('metric1', 100);
instance.recordMetric('metric2', 200);
instance.clearMetrics('metric1');
expect(instance.getCount('metric1')).toBe(0);
expect(instance.getCount('metric2')).toBe(1);
});
it('should clear all metrics when no name provided', () => {
const instance = PerformanceMonitor.getInstance();
instance.recordMetric('metric1', 100);
instance.recordMetric('metric2', 200);
instance.recordMetric('metric3', 300);
instance.clearMetrics();
expect(instance.getCount('metric1')).toBe(0);
expect(instance.getCount('metric2')).toBe(0);
expect(instance.getCount('metric3')).toBe(0);
});
});
});
+80
View File
@@ -0,0 +1,80 @@
import { sanitizeHTML, sanitizeInput, sanitizeURL, escapeHTML } from './sanitize';
describe('sanitize', () => {
describe('sanitizeHTML', () => {
it('should allow safe HTML tags', () => {
const result = sanitizeHTML('<p>Hello <b>world</b></p>');
expect(result).toContain('<p>');
expect(result).toContain('<b>');
});
it('should remove dangerous tags', () => {
const result = sanitizeHTML('<script>alert("xss")</script><p>safe</p>');
expect(result).not.toContain('<script>');
expect(result).toContain('<p>');
});
it('should remove dangerous attributes', () => {
const result = sanitizeHTML('<a href="#" onclick="alert(1)">link</a>');
expect(result).not.toContain('onclick');
});
it('should handle empty input', () => {
expect(sanitizeHTML('')).toBe('');
});
});
describe('sanitizeInput', () => {
it('should remove all HTML tags', () => {
const result = sanitizeInput('<p>Hello <b>world</b></p>');
expect(result).not.toContain('<p>');
expect(result).not.toContain('<b>');
expect(result).toContain('Hello');
expect(result).toContain('world');
});
it('should handle special characters', () => {
const result = sanitizeInput('<script>alert("xss")</script>');
expect(result).not.toContain('<script>');
});
});
describe('sanitizeURL', () => {
it('should allow valid http URLs', () => {
expect(sanitizeURL('http://example.com')).toBe('http://example.com');
});
it('should allow valid https URLs', () => {
expect(sanitizeURL('https://example.com')).toBe('https://example.com');
});
it('should allow mailto URLs', () => {
expect(sanitizeURL('mailto:test@example.com')).toBe('mailto:test@example.com');
});
it('should reject javascript URLs', () => {
expect(sanitizeURL('javascript:alert(1)')).toBe('');
});
it('should reject data URLs', () => {
expect(sanitizeURL('data:text/html,<script>alert(1)</script>')).toBe('');
});
});
describe('escapeHTML', () => {
it('should escape HTML special characters', () => {
expect(escapeHTML('<div>')).toBe('&lt;div&gt;');
expect(escapeHTML('&')).toBe('&amp;');
expect(escapeHTML('"')).toBe('&quot;');
expect(escapeHTML("'")).toBe('&#x27;');
});
it('should handle mixed content', () => {
expect(escapeHTML('<script>alert("test")</script>')).toBe('&lt;script&gt;alert(&quot;test&quot;)&lt;&#x2F;script&gt;');
});
it('should handle empty string', () => {
expect(escapeHTML('')).toBe('');
});
});
});
+53
View File
@@ -0,0 +1,53 @@
import { initSentry } from './sentry';
describe('sentry', () => {
const originalEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
console.error = jest.fn();
});
afterEach(() => {
process.env = originalEnv;
});
describe('initSentry', () => {
it('should not initialize Sentry in non-production environment', () => {
process.env.NODE_ENV = 'development';
process.env.NEXT_PUBLIC_SENTRY_DSN = 'test-dsn';
initSentry();
expect(console.error).not.toHaveBeenCalled();
});
it('should not initialize Sentry when DSN is not set', () => {
process.env.NODE_ENV = 'production';
process.env.NEXT_PUBLIC_SENTRY_DSN = '';
initSentry();
expect(console.error).not.toHaveBeenCalled();
});
it('should initialize Sentry in production with DSN', () => {
process.env.NODE_ENV = 'production';
process.env.NEXT_PUBLIC_SENTRY_DSN = 'https://test@sentry.io/123';
initSentry();
expect(console.error).not.toHaveBeenCalled();
});
it('should handle missing NODE_ENV gracefully', () => {
delete process.env.NODE_ENV;
process.env.NEXT_PUBLIC_SENTRY_DSN = 'https://test@sentry.io/123';
initSentry();
expect(console.error).not.toHaveBeenCalled();
});
});
});
+142
View File
@@ -0,0 +1,142 @@
import { cn, formatNumber, formatCurrency, debounce, throttle, randomBetween, lerp, clamp } from './utils';
describe('utils', () => {
describe('cn', () => {
it('should merge class names correctly', () => {
expect(cn('foo', 'bar')).toBe('foo bar');
});
it('should handle conditional classes', () => {
expect(cn('foo', false && 'bar', 'baz')).toBe('foo baz');
});
it('should handle empty input', () => {
expect(cn()).toBe('');
});
});
describe('formatNumber', () => {
it('should format numbers with Chinese locale', () => {
expect(formatNumber(1234567)).toBe('1,234,567');
});
it('should handle decimal numbers', () => {
expect(formatNumber(1234.56)).toBe('1,234.56');
});
it('should handle zero', () => {
expect(formatNumber(0)).toBe('0');
});
});
describe('formatCurrency', () => {
it('should format numbers as CNY currency', () => {
expect(formatCurrency(1234.56)).toBe('¥1,234.56');
});
it('should handle large numbers', () => {
expect(formatCurrency(1000000)).toBe('¥1,000,000.00');
});
it('should handle zero', () => {
expect(formatCurrency(0)).toBe('¥0.00');
});
});
describe('debounce', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should delay function execution', () => {
const mockFn = jest.fn();
const debouncedFn = debounce(mockFn, 100);
debouncedFn();
expect(mockFn).not.toHaveBeenCalled();
jest.advanceTimersByTime(100);
expect(mockFn).toHaveBeenCalledTimes(1);
});
it('should cancel previous calls', () => {
const mockFn = jest.fn();
const debouncedFn = debounce(mockFn, 100);
debouncedFn();
jest.advanceTimersByTime(50);
debouncedFn();
jest.advanceTimersByTime(100);
expect(mockFn).toHaveBeenCalledTimes(1);
});
});
describe('throttle', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should limit function execution rate', () => {
const mockFn = jest.fn();
const throttledFn = throttle(mockFn, 100);
throttledFn();
throttledFn();
throttledFn();
expect(mockFn).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(100);
throttledFn();
expect(mockFn).toHaveBeenCalledTimes(2);
});
});
describe('randomBetween', () => {
it('should generate number in range', () => {
const result = randomBetween(1, 10);
expect(result).toBeGreaterThanOrEqual(1);
expect(result).toBeLessThanOrEqual(10);
});
it('should handle negative numbers', () => {
const result = randomBetween(-10, -1);
expect(result).toBeGreaterThanOrEqual(-10);
expect(result).toBeLessThanOrEqual(-1);
});
});
describe('lerp', () => {
it('should interpolate between values', () => {
expect(lerp(0, 10, 0.5)).toBe(5);
expect(lerp(0, 100, 0.25)).toBe(25);
});
it('should handle edge cases', () => {
expect(lerp(0, 10, 0)).toBe(0);
expect(lerp(0, 10, 1)).toBe(10);
});
});
describe('clamp', () => {
it('should clamp values within range', () => {
expect(clamp(5, 0, 10)).toBe(5);
expect(clamp(-5, 0, 10)).toBe(0);
expect(clamp(15, 0, 10)).toBe(10);
});
it('should handle equal min and max', () => {
expect(clamp(5, 5, 5)).toBe(5);
});
});
});