test(hooks): finalize mutation test improvements and release acceptance

- Raise use-swipe-gesture mutation score to 66.13% (target 65%+)
- Maintain use-reduced-motion mutation score at 76.32% (target 50%+)
- Fix use-reduced-motion.ts ESLint set-state-in-effect warning
- Add UJ-10 deep searcher journey (category browse → article read → content discovery)
- Add 40 new test files (analytics, detail, sections, ui, lib components)
- Update test-strategy-plan.md to v2.0 (sync test count to 1591)
- Sync README.md with final release metrics

Quality gates: TS 0 errors, ESLint 0 errors, 121 suites / 1591 tests passed
This commit is contained in:
2026-08-02 19:39:27 +08:00
parent 7c0af54897
commit 602ed6a671
203 changed files with 8338 additions and 81 deletions
+121
View File
@@ -0,0 +1,121 @@
// @ts-nocheck
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, ...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}
{...props}
>
{children}
</div>
),
},
useInView: jest.fn(() => true),
AnimatePresence: ({ children }: any) => <>{children}</>,
}));
// ─── Mock Data ────────────────────────────────────────────────────────────
const mockCase = {
industry: '制造业',
title: '某制造企业数字化转型',
challenge: '库存管理混乱,信息孤岛严重,生产流程不透明',
solution: '实施ERP系统,打通全业务流程,实现数据驱动决策',
impact: [
{ value: '200%', label: '库存周转率提升' },
{ value: '50%', label: '运营成本降低' },
],
quote: '数字化转型让我们的生产效率大幅提升',
author: '张总 / 某制造企业CEO',
image: '/images/cases/manufacturing.jpg',
};
// ─── Tests ───────────────────────────────────────────────────────────────
describe('CaseCard', () => {
it('should render industry tag and title', async () => {
const { CaseCard } = await import('./case-card');
render(<CaseCard {...mockCase} />);
expect(screen.getByText('制造业')).toBeInTheDocument();
expect(screen.getByText('某制造企业数字化转型')).toBeInTheDocument();
});
it('should render challenge and solution', async () => {
const { CaseCard } = await import('./case-card');
render(<CaseCard {...mockCase} />);
expect(screen.getByText('挑战')).toBeInTheDocument();
expect(screen.getByText('方案')).toBeInTheDocument();
expect(screen.getByText('库存管理混乱,信息孤岛严重,生产流程不透明')).toBeInTheDocument();
expect(screen.getByText('实施ERP系统,打通全业务流程,实现数据驱动决策')).toBeInTheDocument();
});
it('should render impact metrics', async () => {
const { CaseCard } = await import('./case-card');
render(<CaseCard {...mockCase} />);
expect(screen.getByText('200%')).toBeInTheDocument();
expect(screen.getByText('库存周转率提升')).toBeInTheDocument();
expect(screen.getByText('50%')).toBeInTheDocument();
expect(screen.getByText('运营成本降低')).toBeInTheDocument();
});
it('should render quote and author', async () => {
const { CaseCard } = await import('./case-card');
render(<CaseCard {...mockCase} />);
expect(screen.getByText(/数字化转型让我们的生产效率大幅提升/)).toBeInTheDocument();
expect(screen.getByText('张总 / 某制造企业CEO')).toBeInTheDocument();
});
it('should render background image when provided', async () => {
const { CaseCard } = await import('./case-card');
const { container } = render(<CaseCard {...mockCase} />);
const img = container.querySelector('img[alt=""]');
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute('src', '/images/cases/manufacturing.jpg');
});
it('should not render background image when not provided', async () => {
const { CaseCard } = await import('./case-card');
const { container } = render(<CaseCard {...mockCase} image={undefined} />);
const img = container.querySelector('img[alt=""]');
expect(img).not.toBeInTheDocument();
});
it('should use context as fallback when challenge is empty', async () => {
const { CaseCard } = await import('./case-card');
render(
<CaseCard
industry="测试"
title="Test"
challenge=""
solution=""
context="后备上下文"
impact={mockCase.impact}
quote="quote"
author="author"
/>
);
expect(screen.getByText('后备上下文')).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { CaseCard } = await import('./case-card');
const { container } = render(<CaseCard {...mockCase} className="custom-class" />);
const outer = container.querySelector('.custom-class');
expect(outer).toBeInTheDocument();
});
});
@@ -0,0 +1,91 @@
// @ts-nocheck
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, className, ...props }: any) => (
<div
data-testid="motion-div"
data-initial={JSON.stringify(initial)}
data-animate={JSON.stringify(animate)}
data-variants={JSON.stringify(variants)}
data-while-inview={JSON.stringify(whileInView)}
className={className}
{...props}
>
{children}
</div>
),
},
useInView: jest.fn(() => true),
AnimatePresence: ({ children }: any) => <>{children}</>,
}));
// ─── Mock Data ────────────────────────────────────────────────────────────
const FactoryIcon = () => <svg data-testid="icon-factory" className="w-7 h-7" />;
const RetailIcon = () => <svg data-testid="icon-retail" className="w-7 h-7" />;
const EducationIcon = () => <svg data-testid="icon-education" className="w-7 h-7" />;
const HealthcareIcon = () => <svg data-testid="icon-healthcare" className="w-7 h-7" />;
const mockItems = [
{ icon: FactoryIcon, name: '制造业', count: '12个案例' },
{ icon: RetailIcon, name: '贸易零售', count: '8个案例' },
{ icon: EducationIcon, name: '教育培训', count: '6个案例' },
{ icon: HealthcareIcon, name: '医疗健康', count: '5个案例' },
];
// ─── Tests ───────────────────────────────────────────────────────────────
describe('IndustryGrid', () => {
it('should render all industry items', async () => {
const { IndustryGrid } = await import('./industry-grid');
render(<IndustryGrid items={mockItems} />);
expect(screen.getByText('制造业')).toBeInTheDocument();
expect(screen.getByText('贸易零售')).toBeInTheDocument();
expect(screen.getByText('教育培训')).toBeInTheDocument();
expect(screen.getByText('医疗健康')).toBeInTheDocument();
});
it('should render case counts', async () => {
const { IndustryGrid } = await import('./industry-grid');
render(<IndustryGrid items={mockItems} />);
expect(screen.getByText('12个案例')).toBeInTheDocument();
expect(screen.getByText('8个案例')).toBeInTheDocument();
expect(screen.getByText('6个案例')).toBeInTheDocument();
expect(screen.getByText('5个案例')).toBeInTheDocument();
});
it('should render icons for each item', async () => {
const { IndustryGrid } = await import('./industry-grid');
render(<IndustryGrid items={mockItems} />);
expect(screen.getByTestId('icon-factory')).toBeInTheDocument();
expect(screen.getByTestId('icon-retail')).toBeInTheDocument();
expect(screen.getByTestId('icon-education')).toBeInTheDocument();
expect(screen.getByTestId('icon-healthcare')).toBeInTheDocument();
});
it('should render with role="button" and tabIndex={0}', async () => {
const { IndustryGrid } = await import('./industry-grid');
render(<IndustryGrid items={mockItems.slice(0, 1)} />);
const item = screen.getByRole('button');
expect(item).toHaveAttribute('tabindex', '0');
});
it('should apply custom className', async () => {
const { IndustryGrid } = await import('./industry-grid');
const { container } = render(<IndustryGrid items={mockItems} className="custom-grid" />);
const grid = container.querySelector('.custom-grid');
expect(grid).toBeInTheDocument();
});
it('should render empty grid when items is empty', async () => {
const { IndustryGrid } = await import('./industry-grid');
const { container } = render(<IndustryGrid items={[]} />);
expect(container.querySelector('[role="button"]')).not.toBeInTheDocument();
});
});
@@ -0,0 +1,96 @@
// @ts-nocheck
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, viewport, transition, className, ...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-inview={JSON.stringify(whileInView)}
data-viewport={JSON.stringify(viewport)}
data-transition={JSON.stringify(transition)}
className={className}
{...props}
>
{children}
</div>
),
},
useInView: jest.fn(() => true),
AnimatePresence: ({ children }: any) => <>{children}</>,
}));
jest.mock('lucide-react', () => ({
ArrowUpRight: (props: any) => <svg data-testid="icon-arrow-up-right" className={props.className} />,
}));
// ─── Mock Data ────────────────────────────────────────────────────────────
const mockInsight = {
tag: '白皮书',
title: '2025年数字化转型趋势',
desc: '深入分析企业数字化转型的最新趋势与挑战',
date: '2025年1月',
};
// ─── Tests ───────────────────────────────────────────────────────────────
describe('InsightCard', () => {
it('should render tag, title, and description', async () => {
const { InsightCard } = await import('./insight-card');
render(<InsightCard {...mockInsight} />);
expect(screen.getByText('白皮书')).toBeInTheDocument();
expect(screen.getByText('2025年数字化转型趋势')).toBeInTheDocument();
expect(screen.getByText('深入分析企业数字化转型的最新趋势与挑战')).toBeInTheDocument();
});
it('should render date when provided', async () => {
const { InsightCard } = await import('./insight-card');
render(<InsightCard {...mockInsight} />);
expect(screen.getByText('2025年1月')).toBeInTheDocument();
});
it('should not render date when not provided', async () => {
const { InsightCard } = await import('./insight-card');
render(<InsightCard {...mockInsight} date={undefined} />);
expect(screen.queryByText('2025年1月')).not.toBeInTheDocument();
});
it('should render "阅读全文" for featured variant', async () => {
const { InsightCard } = await import('./insight-card');
render(<InsightCard {...mockInsight} featured />);
expect(screen.getByText('阅读全文')).toBeInTheDocument();
});
it('should render background image for featured variant', async () => {
const { InsightCard } = await import('./insight-card');
const { container } = render(
<InsightCard {...mockInsight} featured image="/images/insights/trends.jpg" />
);
const img = container.querySelector('img');
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute('src', '/images/insights/trends.jpg');
});
it('should render link element when provided', async () => {
const { InsightCard } = await import('./insight-card');
render(<InsightCard {...mockInsight} link={<span data-testid="custom-link">查看更多</span>} />);
expect(screen.getByTestId('custom-link')).toBeInTheDocument();
expect(screen.getByText('查看更多')).toBeInTheDocument();
});
it('should apply custom className', async () => {
const { InsightCard } = await import('./insight-card');
const { container } = render(<InsightCard {...mockInsight} className="custom-card" />);
const card = container.querySelector('.custom-card');
expect(card).toBeInTheDocument();
});
});
@@ -0,0 +1,121 @@
// @ts-nocheck
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, ...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}
{...props}
>
{children}
</div>
),
},
useInView: jest.fn(() => true),
AnimatePresence: ({ children }: any) => <>{children}</>,
}));
jest.mock('@/hooks/use-reduced-motion', () => ({
useReducedMotion: jest.fn(() => false),
}));
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', () => ({
ArrowRight: (props: any) => <svg data-testid="icon-arrow-right" className={props.className} />,
}));
// ─── Tests ───────────────────────────────────────────────────────────────
describe('ServiceCard', () => {
it('should render title and description', async () => {
const { ServiceCard } = await import('./service-card');
render(
<ServiceCard
title="战略咨询"
description="企业数字化转型战略规划"
href="/services/consulting"
/>
);
expect(screen.getByText('战略咨询')).toBeInTheDocument();
expect(screen.getByText('企业数字化转型战略规划')).toBeInTheDocument();
});
it('should render "了解详情" link with correct href', async () => {
const { ServiceCard } = await import('./service-card');
render(
<ServiceCard
title="战略咨询"
description="企业数字化转型战略规划"
href="/services/consulting"
/>
);
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
title="Data"
description="Analysis"
href="/services/data"
className="custom-class"
/>
);
const elements = screen.getAllByTestId('motion-div');
const target = elements.find(el => el.className.includes('custom-class'));
expect(target).toBeTruthy();
});
it('should accept optional number and color props', async () => {
const { ServiceCard } = await import('./service-card');
render(
<ServiceCard
number="03"
title="Blue"
description="Service"
href="/services/test"
color="blue"
/>
);
const elements = screen.getAllByTestId('motion-div');
expect(elements.length).toBeGreaterThan(0);
});
it('should render with delay prop', async () => {
const { ServiceCard } = await import('./service-card');
render(
<ServiceCard
title="Delayed"
description="With delay"
href="/services/delayed"
delay={0.2}
/>
);
expect(screen.getByText('Delayed')).toBeInTheDocument();
expect(screen.getByText('With delay')).toBeInTheDocument();
});
});