feat(methodology): add CMS-driven methodology page skeleton

- 新增 methodology 内容模型(content-types + seed),内容由 CMS 后台可编辑
- 新建 /methodology 页面:Hero / 四阶段 Cards / 空态占位 / 轻量 CTA
- 首页「了解我们的方法论」CTA 改指向 /methodology;sitemap 收录
- 单元测试 4/4 + 视觉基线通过,type-check / lint 0 errors
This commit is contained in:
2026-08-19 15:28:15 +08:00
parent c195768281
commit 6573ab449e
9 changed files with 421 additions and 3 deletions
@@ -0,0 +1,121 @@
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, className, style, ...props }: any) => (
<div data-testid="motion-div" className={className} style={style} {...props}>{children}</div>
),
h1: ({ children, className, ...props }: any) => (
<h1 data-testid="motion-h1" className={className} {...props}>{children}</h1>
),
p: ({ children, className, ...props }: any) => (
<p data-testid="motion-p" className={className} {...props}>{children}</p>
),
},
useInView: jest.fn(() => true),
AnimatePresence: ({ children }: any) => <>{children}</>,
}));
jest.mock('@/components/ui/scroll-reveal', () => ({
ScrollReveal: ({ children, className }: any) => (
<div data-testid="scroll-reveal" className={className}>{children}</div>
),
StaggerReveal: ({ children, className }: any) => (
<div data-testid="stagger-reveal" className={className}>{children}</div>
),
}));
jest.mock('@/components/ui/button', () => ({
Button: ({ children, className, ...props }: any) => (
<button className={className} data-testid="mock-button" {...props}>
{children}
</button>
),
}));
jest.mock('@/components/ui/page-decoration', () => ({
EASE_OUT: [0.22, 1, 0.36, 1],
}));
jest.mock('lucide-react', () => {
const mockIcon = (name: string) => {
const Icon = (props: any) => <svg data-testid={`icon-${name.toLowerCase()}`} className={props.className} />;
Icon.displayName = name;
return Icon;
};
return {
ArrowRight: mockIcon('arrow-right'),
CheckCircle2: mockIcon('check-circle2'),
};
});
// ─── Mock Data ────────────────────────────────────────────────────────────
const mockData = {
heroSubtitle: '方法论',
heroTitle: '四阶段推进数字化转型',
heroDescription: '从诊断到优化,步步可衡量。',
phases: [
{
number: 1,
title: '诊断评估',
subtitle: '了解现状,找准痛点',
description: '深入调研企业现状,识别关键痛点。',
activities: ['业务流程调研', 'IT基础设施评估'],
deliverables: ['成熟度评估报告', '关键痛点清单'],
},
{
number: 2,
title: '战略规划',
subtitle: '明确方向,制定路线',
description: '制定匹配企业战略的转型路线图。',
activities: ['IT战略对齐'],
deliverables: ['战略规划书'],
},
],
};
// ─── Tests ────────────────────────────────────────────────────────────────
describe('MethodologyContent - 方法论页面', () => {
it('should render hero section', async () => {
const { MethodologyContent } = await import('./methodology-content');
render(<MethodologyContent data={mockData} />);
expect(screen.getByText('方法论')).toBeInTheDocument();
expect(screen.getByText('四阶段推进数字化转型')).toBeInTheDocument();
expect(screen.getByText('从诊断到优化,步步可衡量。')).toBeInTheDocument();
});
it('should render all methodology phases with activities and deliverables', async () => {
const { MethodologyContent } = await import('./methodology-content');
render(<MethodologyContent data={mockData} />);
expect(screen.getByText('诊断评估')).toBeInTheDocument();
expect(screen.getByText('战略规划')).toBeInTheDocument();
expect(screen.getAllByText('核心活动').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('交付物').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('业务流程调研')).toBeInTheDocument();
expect(screen.getByText('成熟度评估报告')).toBeInTheDocument();
});
it('should render empty state when phases are missing', async () => {
const { MethodologyContent } = await import('./methodology-content');
render(<MethodologyContent data={{ heroTitle: '方法论' }} />);
expect(screen.getByText('方法论内容即将发布')).toBeInTheDocument();
});
it('should render CTA links to services and contact', async () => {
const { MethodologyContent } = await import('./methodology-content');
render(<MethodologyContent data={mockData} />);
const links = screen.getAllByRole('link');
expect(links.some((l) => l.getAttribute('href') === '/services')).toBe(true);
expect(links.some((l) => l.getAttribute('href') === '/contact')).toBe(true);
});
});