;
+
+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('无权限执行此操作');
+ });
+ });
+});
diff --git a/src/lib/color-contrast.test.ts b/src/lib/color-contrast.test.ts
new file mode 100644
index 0000000..8df3bb9
--- /dev/null
+++ b/src/lib/color-contrast.test.ts
@@ -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');
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/lib/colors.test.ts b/src/lib/colors.test.ts
new file mode 100644
index 0000000..b36a14b
--- /dev/null
+++ b/src/lib/colors.test.ts
@@ -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();
+ });
+ });
+});
diff --git a/src/lib/constants.test.ts b/src/lib/constants.test.ts
new file mode 100644
index 0000000..ec40cad
--- /dev/null
+++ b/src/lib/constants.test.ts
@@ -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();
+ });
+ });
+});
diff --git a/src/lib/csrf.test.ts b/src/lib/csrf.test.ts
new file mode 100644
index 0000000..4689971
--- /dev/null
+++ b/src/lib/csrf.test.ts
@@ -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');
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/lib/email-templates.test.ts b/src/lib/email-templates.test.ts
new file mode 100644
index 0000000..5895872
--- /dev/null
+++ b/src/lib/email-templates.test.ts
@@ -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('');
+ expect(email).toContain('');
+ expect(email).toContain('');
+ });
+
+ 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: '张三 ' };
+ const email = generateNotificationEmail(data);
+
+ expect(email).toContain('张三 ');
+ });
+
+ 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('');
+ expect(email).toContain('');
+ expect(email).toContain('');
+ });
+
+ 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: '张三 ' };
+ const email = generateConfirmationEmail(data);
+
+ expect(email).toContain('张三 ');
+ });
+ });
+
+ 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);
+ });
+ });
+});
diff --git a/src/lib/gradients.test.ts b/src/lib/gradients.test.ts
new file mode 100644
index 0000000..90aea9a
--- /dev/null
+++ b/src/lib/gradients.test.ts
@@ -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');
+ });
+ });
+});
diff --git a/src/lib/integration.test.ts b/src/lib/integration.test.ts
new file mode 100644
index 0000000..2af75ac
--- /dev/null
+++ b/src/lib/integration.test.ts
@@ -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 = 'Hello World';
+ const sanitized = sanitizeInput(userInput);
+
+ expect(sanitized).not.toContain('';
+ const sanitized = sanitizeHTML(htmlContent);
+
+ expect(sanitized).not.toContain('onclick');
+ expect(sanitized).not.toContain('John',
+ email: 'john@example.com',
+ website: 'javascript:alert(1)',
+ message: 'Hello
',
+ };
+
+ const sanitized = {
+ name: sanitizeInput(maliciousInput.name),
+ email: sanitizeInput(maliciousInput.email),
+ website: sanitizeURL(maliciousInput.website),
+ message: sanitizeHTML(maliciousInput.message),
+ };
+
+ expect(sanitized.name).not.toContain('safe
');
+ expect(result).not.toContain('');
+ expect(result).not.toContain('')).toBe('');
+ });
+ });
+
+ describe('escapeHTML', () => {
+ it('should escape HTML special characters', () => {
+ expect(escapeHTML('')).toBe('<div>');
+ expect(escapeHTML('&')).toBe('&');
+ expect(escapeHTML('"')).toBe('"');
+ expect(escapeHTML("'")).toBe(''');
+ });
+
+ it('should handle mixed content', () => {
+ expect(escapeHTML('')).toBe('<script>alert("test")</script>');
+ });
+
+ it('should handle empty string', () => {
+ expect(escapeHTML('')).toBe('');
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/lib/sentry.test.ts b/src/lib/sentry.test.ts
new file mode 100644
index 0000000..50c4933
--- /dev/null
+++ b/src/lib/sentry.test.ts
@@ -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();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts
new file mode 100644
index 0000000..4b3511a
--- /dev/null
+++ b/src/lib/utils.test.ts
@@ -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);
+ });
+ });
+});
\ No newline at end of file