feat: 实现动态详情页面和性能优化
- 添加案例、新闻、产品详情页面的E2E测试 - 优化详情页面的客户端组件和页面逻辑 - 添加高性能Docker配置和Nginx配置 - 更新API服务和常量配置 - 添加性能优化文档和任务进度更新 - 修复ESLint错误和类型问题
This commit is contained in:
@@ -5,24 +5,17 @@ import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { BackButton } from '@/components/ui/back-button';
|
||||
import { CheckCircle2, TrendingUp, Users, Target, Quote, Clock, MessageCircle, Award } from 'lucide-react';
|
||||
import { CASES } from '@/lib/constants';
|
||||
import type { StaticImageData } from 'next/image';
|
||||
|
||||
interface CaseResult {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
import { Users, Target, Quote, Clock, MessageCircle, Award, TrendingUp } from 'lucide-react';
|
||||
|
||||
interface CaseItem {
|
||||
id: string;
|
||||
title: string;
|
||||
client: string;
|
||||
industry: string;
|
||||
description: string;
|
||||
results: readonly CaseResult[];
|
||||
tags: readonly string[];
|
||||
image?: string | StaticImageData;
|
||||
excerpt: string;
|
||||
content: string;
|
||||
category: string;
|
||||
slug: string;
|
||||
publishedAt?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface CaseDetailClientProps {
|
||||
@@ -50,20 +43,6 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const relatedCases = CASES.filter((c) => c.id !== caseItem.id).slice(0, 2);
|
||||
|
||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
'业务处理效率': TrendingUp,
|
||||
'客户满意度': Users,
|
||||
'运营成本': Target,
|
||||
'生产效率': TrendingUp,
|
||||
'设备利用率': Target,
|
||||
'不良品率': CheckCircle2,
|
||||
'数据整合效率': TrendingUp,
|
||||
'决策响应时间': Target,
|
||||
'营销转化率': Users,
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<div className="relative overflow-hidden bg-gradient-to-b from-[#FAFAFA] to-white">
|
||||
@@ -71,13 +50,13 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
|
||||
<BackButton />
|
||||
<div className="max-w-4xl mt-8">
|
||||
<Badge className="mb-4 bg-[#C41E3A]/10 text-[#C41E3A] hover:bg-[#C41E3A]/20">
|
||||
{caseItem.industry}
|
||||
{caseItem.category}
|
||||
</Badge>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-semibold text-[#1C1C1C] mb-2">
|
||||
{caseItem.title}
|
||||
</h1>
|
||||
<p className="text-lg text-[#5C5C5C]">
|
||||
{caseItem.client}
|
||||
{caseItem.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,11 +81,11 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-[#5C5C5C] leading-relaxed text-lg">
|
||||
{caseItem.description}
|
||||
{caseItem.excerpt}
|
||||
</p>
|
||||
<div className="mt-6 p-4 bg-white rounded-lg border border-[#E5E5E5]">
|
||||
<p className="text-sm text-[#737373] italic">
|
||||
"在找到睿新致远之前,我们面临着巨大的挑战..."
|
||||
“在找到睿新致远之前,我们面临着巨大的挑战...”
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -120,20 +99,8 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
|
||||
我们如何智连未来
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{caseItem.tags.map((tag, index) => (
|
||||
<div key={tag} className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-[#C41E3A]/10 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-[#C41E3A] font-semibold">{index + 1}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-[#1C1C1C] mb-1">{tag}</h3>
|
||||
<p className="text-sm text-[#737373]">
|
||||
基于 {tag} 技术的专业解决方案,助力企业实现数字化转型目标。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<div dangerouslySetInnerHTML={{ __html: caseItem.content }} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -182,25 +149,31 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-3 gap-4 mb-6">
|
||||
{caseItem.results.map((result) => {
|
||||
const Icon = iconMap[result.label] || TrendingUp;
|
||||
return (
|
||||
<div
|
||||
key={result.label}
|
||||
className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A] transition-colors"
|
||||
>
|
||||
<Icon className="w-8 h-8 text-[#C41E3A] mb-3" />
|
||||
<div className="text-2xl font-semibold text-[#C41E3A] mb-1">
|
||||
{result.value}
|
||||
</div>
|
||||
<div className="text-sm text-[#737373]">{result.label}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A] transition-colors">
|
||||
<TrendingUp className="w-8 h-8 text-[#C41E3A] mb-3" />
|
||||
<div className="text-2xl font-semibold text-[#C41E3A] mb-1">
|
||||
300%
|
||||
</div>
|
||||
<div className="text-sm text-[#737373]">业务处理效率</div>
|
||||
</div>
|
||||
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A] transition-colors">
|
||||
<Users className="w-8 h-8 text-[#C41E3A] mb-3" />
|
||||
<div className="text-2xl font-semibold text-[#C41E3A] mb-1">
|
||||
95%
|
||||
</div>
|
||||
<div className="text-sm text-[#737373]">客户满意度</div>
|
||||
</div>
|
||||
<div className="p-6 bg-white rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A] transition-colors">
|
||||
<Target className="w-8 h-8 text-[#C41E3A] mb-3" />
|
||||
<div className="text-2xl font-semibold text-[#C41E3A] mb-1">
|
||||
-40%
|
||||
</div>
|
||||
<div className="text-sm text-[#737373]">运营成本</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-white rounded-lg border border-[#E5E5E5]">
|
||||
<p className="text-sm text-[#737373] italic">
|
||||
"通过三年的合作,我们不仅实现了数字化转型,更重要的是建立了一个可持续发展的技术体系。"
|
||||
“通过三年的合作,我们不仅实现了数字化转型,更重要的是建立了一个可持续发展的技术体系。”
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -221,10 +194,10 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-[#C41E3A] rounded-full flex items-center justify-center">
|
||||
<span className="text-white font-semibold">{caseItem.client[0]}</span>
|
||||
<span className="text-white font-semibold">客</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-[#1C1C1C]">{caseItem.client}</p>
|
||||
<p className="font-semibold text-[#1C1C1C]">客户企业</p>
|
||||
<p className="text-sm text-[#737373]">CEO</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,25 +211,19 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
|
||||
<dl className="space-y-3">
|
||||
<div>
|
||||
<dt className="text-sm text-[#737373]">客户名称</dt>
|
||||
<dd className="text-[#1C1C1C] font-medium">{caseItem.client}</dd>
|
||||
<dd className="text-[#1C1C1C] font-medium">客户企业</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-[#737373]">行业领域</dt>
|
||||
<dd className="text-[#1C1C1C] font-medium">{caseItem.industry}</dd>
|
||||
<dd className="text-[#1C1C1C] font-medium">{caseItem.category}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-[#737373]">合作时长</dt>
|
||||
<dd className="text-[#1C1C1C] font-medium">3年</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-[#737373]">技术标签</dt>
|
||||
<dd className="flex flex-wrap gap-2 mt-1">
|
||||
{caseItem.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</dd>
|
||||
<dt className="text-sm text-[#737373]">发布时间</dt>
|
||||
<dd className="text-[#1C1C1C] font-medium">{caseItem.publishedAt || caseItem.createdAt}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
@@ -277,31 +244,6 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{relatedCases.length > 0 && (
|
||||
<section className="mt-16 pt-16 border-t border-[#E5E5E5]">
|
||||
<h2 className="text-2xl font-semibold text-[#171717] mb-8">相关案例</h2>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{relatedCases.map((relatedCase) => (
|
||||
<Link
|
||||
key={relatedCase.id}
|
||||
href={`/cases/${relatedCase.id}`}
|
||||
className="group p-6 bg-[#FAFAFA] rounded-lg border border-[#E5E5E5] hover:border-[#C41E3A] transition-colors"
|
||||
>
|
||||
<Badge variant="secondary" className="mb-2">
|
||||
{relatedCase.industry}
|
||||
</Badge>
|
||||
<h3 className="text-lg font-semibold text-[#171717] group-hover:text-[#C41E3A] transition-colors">
|
||||
{relatedCase.title}
|
||||
</h3>
|
||||
<p className="text-sm text-[#737373] mt-2 line-clamp-2">
|
||||
{relatedCase.description}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { CASES } from '@/lib/constants';
|
||||
import { contentService } from '@/lib/api/services';
|
||||
import { CaseDetailClient } from './client';
|
||||
|
||||
interface CaseItem {
|
||||
id: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
content: string;
|
||||
category: string;
|
||||
slug: string;
|
||||
publishedAt?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return CASES.map((caseItem) => ({
|
||||
const cases = await contentService.getCases(100);
|
||||
return cases.map((caseItem) => ({
|
||||
id: caseItem.id,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
const caseItem = CASES.find((c) => c.id === id);
|
||||
const cases = await contentService.getCases(100);
|
||||
const caseItem = cases.find((c) => c.id === id);
|
||||
|
||||
if (!caseItem) {
|
||||
return {
|
||||
@@ -21,17 +34,18 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
||||
|
||||
return {
|
||||
title: `${caseItem.title} - 睿新致远`,
|
||||
description: caseItem.description,
|
||||
description: caseItem.excerpt,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function CaseDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const caseItem = CASES.find((c) => c.id === id);
|
||||
const cases = await contentService.getCases(100);
|
||||
const caseItem = cases.find((c) => c.id === id);
|
||||
|
||||
if (!caseItem) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <CaseDetailClient caseItem={caseItem as any} />;
|
||||
return <CaseDetailClient caseItem={caseItem as CaseItem} />;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
import { describe, it, expect, jest, beforeAll } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
interface MockComponentProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
section: ({ children, className, ...props }: any) => (
|
||||
<section className={className} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
div: function MockDiv({ children, className, ...props }: MockComponentProps) {
|
||||
return <div className={className} {...props}>{children}</div>;
|
||||
},
|
||||
section: function MockSection({ children, className, ...props }: MockComponentProps) {
|
||||
return <section className={className} {...props}>{children}</section>;
|
||||
},
|
||||
},
|
||||
AnimatePresence: function MockAnimatePresence({ children }: { children?: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
useInView: () => [null, true],
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
function MockLink({ children, href, ...props }: MockComponentProps) {
|
||||
return <a href={href as string} {...props}>{children}</a>;
|
||||
}
|
||||
MockLink.propTypes = {
|
||||
children: PropTypes.node,
|
||||
href: PropTypes.string,
|
||||
};
|
||||
return MockLink;
|
||||
});
|
||||
MockLink.displayName = 'MockLink';
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
ArrowRight: () => <span data-testid="arrow-right-icon" />,
|
||||
@@ -33,117 +42,259 @@ jest.mock('lucide-react', () => ({
|
||||
Building2: () => <span data-testid="building-icon" />,
|
||||
Calendar: () => <span data-testid="calendar-icon" />,
|
||||
TrendingUp: () => <span data-testid="trending-up-icon" />,
|
||||
ChevronLeft: () => <span data-testid="chevron-left-icon" />,
|
||||
ChevronRight: () => <span data-testid="chevron-right-icon" />,
|
||||
Filter: () => <span data-testid="filter-icon" />,
|
||||
Search: () => <span data-testid="search-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/button', () => ({
|
||||
Button: ({ children, className, ...props }: any) => (
|
||||
<button className={className} {...props}>
|
||||
jest.mock('@/components/ui/button', () => {
|
||||
function Button({ children, className, variant, ...props }: MockComponentProps) {
|
||||
return <button className={className} data-variant={variant} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
</button>;
|
||||
}
|
||||
Button.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
variant: PropTypes.string,
|
||||
};
|
||||
return Button;
|
||||
});
|
||||
Button.displayName = 'Button';
|
||||
|
||||
jest.mock('@/components/ui/badge', () => ({
|
||||
Badge: ({ children, className, ...props }: any) => (
|
||||
<span className={className} {...props}>
|
||||
jest.mock('@/components/ui/badge', () => {
|
||||
function Badge({ children, className, variant, ...props }: MockComponentProps) {
|
||||
return <span className={className} data-variant={variant} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
</span>;
|
||||
}
|
||||
Badge.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
variant: PropTypes.string,
|
||||
};
|
||||
return Badge;
|
||||
});
|
||||
Badge.displayName = 'Badge';
|
||||
|
||||
jest.mock('@/components/ui/page-header', () => ({
|
||||
PageHeader: ({ title, description }: any) => (
|
||||
<header>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
</header>
|
||||
),
|
||||
}));
|
||||
jest.mock('@/components/ui/input', () => {
|
||||
function Input({ className, ...props }: MockComponentProps) {
|
||||
return <input className={className} {...props} />;
|
||||
}
|
||||
Input.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
return Input;
|
||||
});
|
||||
Input.displayName = 'Input';
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
CASES: [
|
||||
{
|
||||
id: 'case-1',
|
||||
client: '客户A',
|
||||
title: '数字化转型案例',
|
||||
industry: '制造业',
|
||||
description: '帮助客户实现数字化转型',
|
||||
},
|
||||
{
|
||||
id: 'case-2',
|
||||
client: '客户B',
|
||||
title: 'ERP系统实施案例',
|
||||
industry: '零售业',
|
||||
description: 'ERP系统成功实施',
|
||||
},
|
||||
],
|
||||
jest.mock('@/components/ui/page-header', () => {
|
||||
function PageHeader({ title, description }: MockComponentProps) {
|
||||
return (
|
||||
<header>
|
||||
<h1>{title as string}</h1>
|
||||
<p>{description as string}</p>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
PageHeader.propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
};
|
||||
return PageHeader;
|
||||
});
|
||||
PageHeader.displayName = 'PageHeader';
|
||||
|
||||
jest.mock('@/lib/api/services', () => ({
|
||||
contentService: {
|
||||
getNews: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import CasesPage from './page';
|
||||
import { contentService } from '@/lib/api/services';
|
||||
|
||||
const mockCases: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
content: string;
|
||||
category: string;
|
||||
slug: string;
|
||||
date: string;
|
||||
}> = [
|
||||
{
|
||||
id: 'case-1',
|
||||
title: '数字化转型案例',
|
||||
excerpt: '帮助客户实现数字化转型',
|
||||
content: '详细的数字化转型案例内容',
|
||||
category: '制造业',
|
||||
slug: 'digital-transformation',
|
||||
date: '2024-01-15',
|
||||
},
|
||||
{
|
||||
id: 'case-2',
|
||||
title: 'ERP系统实施案例',
|
||||
excerpt: 'ERP系统成功实施',
|
||||
content: '详细的ERP系统实施案例内容',
|
||||
category: '零售业',
|
||||
slug: 'erp-implementation',
|
||||
date: '2024-01-10',
|
||||
},
|
||||
{
|
||||
id: 'case-3',
|
||||
title: '智能制造升级',
|
||||
excerpt: '智能制造系统升级',
|
||||
content: '详细的智能制造升级案例内容',
|
||||
category: '制造业',
|
||||
slug: 'smart-manufacturing',
|
||||
date: '2024-01-05',
|
||||
},
|
||||
];
|
||||
|
||||
describe('CasesPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(contentService.getNews as jest.Mock).mockResolvedValue(mockCases);
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render cases page', () => {
|
||||
const { container } = render(<CasesPage />);
|
||||
const pageContainer = container.querySelector('.min-h-screen');
|
||||
it('should render loading state initially', () => {
|
||||
render(<CasesPage />);
|
||||
expect(screen.getByText('加载中...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render cases page after loading', async () => {
|
||||
render(<CasesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const pageContainer = document.querySelector('.min-h-screen');
|
||||
expect(pageContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page header', () => {
|
||||
it('should render page header', async () => {
|
||||
render(<CasesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const title = screen.getByText(/与谁同行/i);
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render back to home link', () => {
|
||||
it('should render back to home link', async () => {
|
||||
render(<CasesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const backLink = screen.getByText(/返回首页/i);
|
||||
expect(backLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render case cards', () => {
|
||||
it('should render case cards', async () => {
|
||||
render(<CasesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const caseTitles = screen.getAllByRole('heading', { level: 3 });
|
||||
expect(caseTitles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render case categories', () => {
|
||||
it('should render case categories', async () => {
|
||||
render(<CasesPage />);
|
||||
const categories = screen.getByText(/制造业/i);
|
||||
expect(categories).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('全部')).toBeInTheDocument();
|
||||
expect(screen.getByText('金融')).toBeInTheDocument();
|
||||
expect(screen.getByText('制造')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render CTA section', () => {
|
||||
it('should render CTA section', async () => {
|
||||
render(<CasesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const cta = screen.getByText(/准备开始您的数字化转型之旅/i);
|
||||
expect(cta).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should have case detail links', () => {
|
||||
it('should have case detail links', async () => {
|
||||
render(<CasesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const links = screen.getAllByRole('link');
|
||||
const caseLinks = links.filter(link => link.getAttribute('href')?.startsWith('/cases/'));
|
||||
expect(caseLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have contact links', () => {
|
||||
it('should have contact links', async () => {
|
||||
render(<CasesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const contactLinks = screen.getAllByRole('link', { name: /联系我们|立即咨询/i });
|
||||
expect(contactLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', () => {
|
||||
it('should have proper heading hierarchy', async () => {
|
||||
render(<CasesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const h1 = screen.getByRole('heading', { level: 1 });
|
||||
expect(h1).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filtering', () => {
|
||||
it('should render filter buttons', async () => {
|
||||
render(<CasesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByPlaceholderText('搜索案例...')).toBeInTheDocument();
|
||||
expect(screen.getByText('行业筛选:')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should display error message when API fails', async () => {
|
||||
(contentService.getNews as jest.Mock).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
render(<CasesPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('加载案例失败')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user