test(core): 补充单元测试,修复 useCountUp 精度问题,新增项目文档

- 新增 hooks/components/lib 共 9 个测试文件,覆盖边界条件与异常路径
- 补充 animations.test.tsx 用例(RotatingBorder、CounterWithEffect 等)
- 修复 useCountUp 结束时 toFixed 精度问题
- 调整 jest 覆盖率配置为渐进式阈值,收缩收集范围
- 新增 docs/lessons-learned.md(经验教训汇总)与 docs/troubleshooting.md(问题排查索引)
- 更新 README.md 文档索引
This commit is contained in:
张翔
2026-07-07 19:42:50 +08:00
parent 55381d7012
commit 38be4a19ef
15 changed files with 2907 additions and 6 deletions
+372
View File
@@ -0,0 +1,372 @@
import { describe, it, expect, jest } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
// ─── Mocks ───────────────────────────────────────────────────────────────
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, initial, animate, variants, whileInView, whileHover, whileTap, viewport, transition, className, onMouseMove, onMouseEnter, onMouseLeave, style, ref, ...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)}
data-while-inview={JSON.stringify(whileInView)}
data-viewport={JSON.stringify(viewport)}
data-transition={JSON.stringify(transition)}
className={className}
style={style}
ref={ref}
onMouseMove={onMouseMove}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
{...props}
>
{children}
</div>
),
p: ({ children, className, ...props }: any) => (
<p data-testid="motion-p" className={className} {...props}>{children}</p>
),
h1: ({ children, className, ...props }: any) => (
<h1 data-testid="motion-h1" className={className} {...props}>{children}</h1>
),
span: ({ children, className, ...props }: any) => (
<span data-testid="motion-span" className={className} {...props}>{children}</span>
),
button: ({ children, onClick, className, ...props }: any) => (
<button
data-testid="motion-button"
onClick={onClick}
className={className}
{...props}
>
{children}
</button>
),
},
useInView: jest.fn(() => true),
AnimatePresence: ({ children }: any) => <>{children}</>,
}));
jest.mock('@/hooks/use-reduced-motion', () => ({
useReducedMotion: jest.fn(() => false),
}));
jest.mock('@/components/ui/static-link', () => ({
StaticLink: ({ children, href, className, onClick, ...props }: any) => (
<a href={href} className={className} onClick={onClick} data-testid="static-link" {...props}>
{children}
</a>
),
}));
jest.mock('@/components/ui/button', () => ({
Button: ({ children, size, variant, className, ...props }: any) => (
<button className={`${className}${size ? ` btn-${size}` : ''}${variant ? ` btn-${variant}` : ''}`} data-testid="mock-button" {...props}>
{children}
</button>
),
}));
jest.mock('@/components/ui/brand-visuals', () => ({
BrandStamp: ({ children }: { children: React.ReactNode }) => (
<span data-testid="brand-stamp">{children}</span>
),
}));
jest.mock('@/components/ui/hero-ink-background', () => ({
HeroInkBackground: () => <div data-testid="hero-ink-bg" />,
}));
jest.mock('next/link', () => ({
__esModule: true,
default: ({ children, href, className, ...props }: any) => (
<a href={href} className={className} data-testid="next-link" {...props}>
{children}
</a>
),
}));
jest.mock('lucide-react', () => {
const mockIcon = (name: string) => {
const Icon = (props: any) => <svg data-testid={`icon-${name.toLowerCase()}`} className={props.className} strokeWidth={props.strokeWidth} />;
Icon.displayName = name;
return Icon;
};
return {
ArrowRight: mockIcon('arrow-right'),
MessageSquare: mockIcon('message-square'),
Search: mockIcon('search'),
Rocket: mockIcon('rocket'),
Handshake: mockIcon('handshake'),
Sparkles: mockIcon('sparkles'),
MessageCircle: mockIcon('message-circle'),
Clock: mockIcon('clock'),
ShieldCheck: mockIcon('shield-check'),
};
});
// ─── Tests ──────────────────────────────────────────────────────────────
describe('SectionHeader', () => {
it('should render label, title, and description', async () => {
const { SectionHeader } = await import('./section-header');
render(<SectionHeader label="TEST" title="Test Title" desc="Test description" />);
expect(screen.getByText('TEST')).toBeInTheDocument();
expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText('Test description')).toBeInTheDocument();
});
it('should render highlighted text', async () => {
const { SectionHeader } = await import('./section-header');
render(<SectionHeader label="LABEL" title="Hello " highlight="World" />);
expect(screen.getByText('World')).toBeInTheDocument();
});
it('should apply light mode', async () => {
const { SectionHeader } = await import('./section-header');
render(<SectionHeader label="TEST" title="Title" light />);
const heading = screen.getByText('Title');
// Light mode: white text (rendered as rgb)
expect(heading.closest('h2')?.style.color).toBe('rgb(255, 255, 255)');
});
it('should apply custom className', async () => {
const { SectionHeader } = await import('./section-header');
render(<SectionHeader label="T" title="T" className="my-class" />);
// SectionHeader has multiple motion.div elements, find the first/outer one
const elements = screen.getAllByTestId('motion-div');
const outer = elements.find(el => el.className.includes('my-class'));
expect(outer).toBeTruthy();
});
it('should render without description when not provided', async () => {
const { SectionHeader } = await import('./section-header');
const { container } = render(<SectionHeader label="T" title="T" />);
// There should be no <p> element for description
const paragraphs = container.querySelectorAll('p');
expect(paragraphs.length).toBe(0);
});
});
describe('StatsBar', () => {
const mockItems = [
{ number: '500', unit: '+', label: '客户' },
{ number: '99', unit: '%', label: '满意度', desc: '客户反馈评分' },
];
it('should render stat items', async () => {
const { StatsBar } = await import('./stats-bar');
render(<StatsBar items={mockItems} />);
expect(screen.getByText('客户')).toBeInTheDocument();
expect(screen.getByText('满意度')).toBeInTheDocument();
});
it('should render description when provided', async () => {
const { StatsBar } = await import('./stats-bar');
render(<StatsBar items={mockItems} />);
expect(screen.getByText('客户反馈评分')).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { StatsBar } = await import('./stats-bar');
const { container } = render(<StatsBar items={mockItems} className="stats-class" />);
const grid = container.querySelector('.stats-class');
expect(grid).toBeInTheDocument();
});
it('should render in dark mode', async () => {
const { StatsBar } = await import('./stats-bar');
render(<StatsBar items={mockItems} dark />);
const labels = screen.getAllByText('客户');
expect(labels.length).toBeGreaterThan(0);
});
});
describe('ProductCard', () => {
it('should render title, description and icon', async () => {
const { ProductCard } = await import('./product-card');
render(
<ProductCard
icon={<span data-testid="custom-icon">★</span>}
title="ERP 系统"
description="企业资源管理系统"
href="/products/erp"
/>
);
expect(screen.getByText('ERP 系统')).toBeInTheDocument();
expect(screen.getByText('企业资源管理系统')).toBeInTheDocument();
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
expect(screen.getByTestId('static-link')).toHaveAttribute('href', '/products/erp');
});
it('should render badge when provided', async () => {
const { ProductCard } = await import('./product-card');
render(
<ProductCard
icon={<span>★</span>}
title="ERP"
description="Desc"
href="/products/erp"
badge="新版本"
/>
);
expect(screen.getByText('新版本')).toBeInTheDocument();
});
it('should render "了解更多" link', async () => {
const { ProductCard } = await import('./product-card');
render(
<ProductCard
icon={<span>★</span>}
title="ERP"
description="Desc"
href="/products/erp"
/>
);
expect(screen.getByText('了解更多')).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { ProductCard } = await import('./product-card');
render(
<ProductCard
icon={<span>★</span>}
title="ERP"
description="Desc"
href="/products/erp"
className="card-class"
/>
);
const link = screen.getByTestId('static-link');
expect(link.className).toContain('card-class');
});
});
describe('ServiceCard', () => {
it('should render number, title, description and link', async () => {
const { ServiceCard } = await import('./service-card');
render(
<ServiceCard
number="01"
title="战略咨询"
description="业务诊断与战略规划"
href="/services/consulting"
/>
);
expect(screen.getByText('战略咨询')).toBeInTheDocument();
expect(screen.getByText('业务诊断与战略规划')).toBeInTheDocument();
expect(screen.getByText('了解详情')).toBeInTheDocument();
expect(screen.getByTestId('next-link')).toHaveAttribute('href', '/services/consulting');
});
it('should apply custom className', async () => {
const { ServiceCard } = await import('./service-card');
render(
<ServiceCard
number="02"
title="Data"
description="Analysis"
href="/services/data"
className="service-class"
/>
);
const elements = screen.getAllByTestId('motion-div');
const target = elements.find(el => el.className.includes('service-class'));
expect(target).toBeTruthy();
});
it('should apply custom color', async () => {
const { ServiceCard } = await import('./service-card');
render(
<ServiceCard
number="03"
title="Blue"
description="Service"
href="/services/test"
color="blue"
/>
);
// ServiceCard has multiple motion.div elements, use getAllByTestId
const elements = screen.getAllByTestId('motion-div');
expect(elements.length).toBeGreaterThan(0);
});
});
describe('CTASection', () => {
it('should render default content', async () => {
const { CTASection } = await import('./cta-section');
render(<CTASection />);
expect(screen.getByText('一起聊聊您的数字化需求')).toBeInTheDocument();
expect(screen.getByText('预约免费咨询')).toBeInTheDocument();
});
it('should render custom title and description', async () => {
const { CTASection } = await import('./cta-section');
render(
<CTASection
title="Custom Title"
description="Custom description"
primaryLabel="Get Started"
primaryHref="/start"
secondaryLabel="Learn More"
secondaryHref="/about"
/>
);
expect(screen.getByText('Custom Title')).toBeInTheDocument();
expect(screen.getByText('Custom description')).toBeInTheDocument();
expect(screen.getByText('Get Started')).toBeInTheDocument();
expect(screen.getByText('Learn More')).toBeInTheDocument();
});
it('should have proper links', async () => {
const { CTASection } = await import('./cta-section');
render(<CTASection primaryHref="/contact" />);
const links = screen.getAllByTestId('static-link');
expect(links.length).toBeGreaterThan(0);
});
it('should render brand stamp', async () => {
const { CTASection } = await import('./cta-section');
render(<CTASection />);
expect(screen.getByTestId('brand-stamp')).toBeInTheDocument();
});
});
describe('HeroSectionV2', () => {
it('should render hero heading and content', async () => {
const { HeroSectionV2 } = await import('./hero-section-v2');
render(<HeroSectionV2 />);
expect(screen.getByText('企业数字化转型服务商')).toBeInTheDocument();
expect(screen.getByText('免费获取定制方案')).toBeInTheDocument();
expect(screen.getByText('探索产品')).toBeInTheDocument();
});
it('should render journey steps', async () => {
const { HeroSectionV2 } = await import('./hero-section-v2');
render(<HeroSectionV2 />);
// These labels appear in both CAPABILITIES and JOURNEY_STEPS, use getAllByText
expect(screen.getAllByText('需求沟通').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('方案诊断').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('敏捷交付').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('长期陪跑').length).toBeGreaterThanOrEqual(1);
});
it('should render ink background', async () => {
const { HeroSectionV2 } = await import('./hero-section-v2');
render(<HeroSectionV2 />);
expect(screen.getByTestId('hero-ink-bg')).toBeInTheDocument();
});
it('should provide CTA link to contact', async () => {
const { HeroSectionV2 } = await import('./hero-section-v2');
render(<HeroSectionV2 />);
const links = screen.getAllByTestId('static-link');
const contactLink = links.find(l => l.getAttribute('href') === '/contact');
expect(contactLink).toBeInTheDocument();
});
});