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:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user