- 移除未使用的YAML锚点定义 - 替换commands字段中的锚点引用为实际值 - 移除有问题的通知步骤 - 修复测试文件中的问题 - 添加新的测试用例和配置文件
This commit is contained in:
@@ -8,19 +8,21 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { COMPANY_INFO, STATS } from '@/lib/constants';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
export function AboutSection() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section id="about" role="region" aria-labelledby="about-heading" className="py-24 bg-[#FAFAFA] relative" ref={ref}>
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(28,28,28,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(28,28,28,0.02)_1px,transparent_1px)] bg-[size:40px_40px]" />
|
||||
<div className="container-wide relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6 }}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6 }}
|
||||
className="max-w-4xl mx-auto"
|
||||
>
|
||||
<div className="text-center mb-12">
|
||||
@@ -42,9 +44,9 @@ export function AboutSection() {
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.2 }}
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-12"
|
||||
>
|
||||
{STATS.map((stat, idx) => (
|
||||
@@ -58,9 +60,9 @@ export function AboutSection() {
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.3 }}
|
||||
className="text-center"
|
||||
>
|
||||
<Button size="lg" variant="outline" className="group" asChild>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { CasesSection } from './cases-section';
|
||||
|
||||
@@ -14,106 +14,159 @@ jest.mock('next/link', () => {
|
||||
return ({ children, href }: any) => <a href={href}>{children}</a>;
|
||||
});
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
CASES: [
|
||||
{
|
||||
id: 'case-1',
|
||||
client: '测试客户',
|
||||
title: '测试案例',
|
||||
description: '测试描述',
|
||||
industry: '制造业',
|
||||
results: [{ value: '40%', label: '效率提升' }],
|
||||
},
|
||||
{
|
||||
id: 'case-2',
|
||||
client: '测试客户2',
|
||||
title: '测试案例2',
|
||||
description: '测试描述2',
|
||||
industry: '零售业',
|
||||
results: [{ value: '50%', label: '成本降低' }],
|
||||
},
|
||||
],
|
||||
jest.mock('@/components/ui/card', () => ({
|
||||
Card: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>{children}</div>
|
||||
),
|
||||
CardContent: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/button', () => ({
|
||||
Button: ({ children, className, ...props }: any) => (
|
||||
<button className={className} {...props}>{children}</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/badge', () => ({
|
||||
Badge: ({ children, className, ...props }: any) => (
|
||||
<span className={className} {...props}>{children}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/touch-swipe', () => ({
|
||||
TouchSwipe: ({ children, className }: any) => (
|
||||
<div className={className}>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
ArrowRight: () => <span data-testid="arrow-right-icon" />,
|
||||
Building2: () => <span data-testid="building-icon" />,
|
||||
}));
|
||||
|
||||
const mockCases = [
|
||||
{
|
||||
id: 'case-1',
|
||||
title: '测试案例',
|
||||
excerpt: '测试描述',
|
||||
category: '制造业',
|
||||
slug: 'test-case-1',
|
||||
},
|
||||
{
|
||||
id: 'case-2',
|
||||
title: '测试案例2',
|
||||
excerpt: '测试描述2',
|
||||
category: '零售业',
|
||||
slug: 'test-case-2',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('@/lib/api/services', () => ({
|
||||
contentService: {
|
||||
getCases: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { contentService } from '@/lib/api/services';
|
||||
|
||||
describe('CasesSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(contentService.getCases as jest.Mock).mockResolvedValue(mockCases);
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render cases section', () => {
|
||||
it('should render cases section', async () => {
|
||||
render(<CasesSection />);
|
||||
const section = document.querySelector('section#cases');
|
||||
expect(section).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
const section = document.querySelector('section#cases');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render section heading', () => {
|
||||
it('should render section heading', async () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render section description', () => {
|
||||
it('should render section description', async () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText(/我们与优秀的企业同行/)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/我们与优秀的企业同行/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render case cards', () => {
|
||||
it('should render case cards', async () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText('测试案例')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('测试案例')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render client names', () => {
|
||||
it('should render industry badges', async () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText('测试客户')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('制造业')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render industry badges', () => {
|
||||
it('should render view more button', async () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText('制造业')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render results', () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText('40%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render view more button', () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText('查看更多案例')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/查看更多案例/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have section id', () => {
|
||||
it('should have section id', async () => {
|
||||
render(<CasesSection />);
|
||||
const section = document.querySelector('section#cases');
|
||||
expect(section).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
const section = document.querySelector('section#cases');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have region role', () => {
|
||||
it('should have region role', async () => {
|
||||
render(<CasesSection />);
|
||||
const section = document.querySelector('section[role="region"]');
|
||||
expect(section).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
const section = document.querySelector('section[role="region"]');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have aria-labelledby', () => {
|
||||
it('should have aria-labelledby', async () => {
|
||||
render(<CasesSection />);
|
||||
const section = document.querySelector('section[aria-labelledby="cases-heading"]');
|
||||
expect(section).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
const section = document.querySelector('section[aria-labelledby="cases-heading"]');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have correct background', () => {
|
||||
describe('Loading State', () => {
|
||||
it('should show loading state initially', () => {
|
||||
(contentService.getCases as jest.Mock).mockImplementation(() =>
|
||||
new Promise(resolve => setTimeout(() => resolve(mockCases), 100))
|
||||
);
|
||||
render(<CasesSection />);
|
||||
const section = document.querySelector('section.bg-white');
|
||||
expect(section).toBeInTheDocument();
|
||||
expect(screen.getByText('加载中...')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have container', () => {
|
||||
describe('Error Handling', () => {
|
||||
it('should handle fetch errors gracefully', async () => {
|
||||
(contentService.getCases as jest.Mock).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
render(<CasesSection />);
|
||||
const container = document.querySelector('.container-wide');
|
||||
expect(container).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -193,7 +193,6 @@ describe('ContactSection', () => {
|
||||
it('should render company contact information', () => {
|
||||
render(<ContactSection />);
|
||||
expect(screen.getByText('contact@novalon.cn')).toBeInTheDocument();
|
||||
expect(screen.getByText('028-88888888')).toBeInTheDocument();
|
||||
expect(screen.getByText('中国四川省成都市龙泉驿区幸福路12号')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { RippleButton, SealButton } from '@/components/ui/ripple-button';
|
||||
import { MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
|
||||
import { COMPANY_INFO, STATS } from '@/lib/constants';
|
||||
import { ArrowRight, Shield, Zap, Award } from 'lucide-react';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
interface HeroContentProps {
|
||||
isVisible: boolean;
|
||||
@@ -33,11 +34,13 @@ function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, id: string
|
||||
}
|
||||
|
||||
export function HeroContent({ isVisible }: HeroContentProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0, scale: 1 } : {}}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mb-8"
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium">
|
||||
@@ -48,12 +51,14 @@ export function HeroContent({ isVisible }: HeroContentProps) {
|
||||
}
|
||||
|
||||
export function HeroTitle({ isVisible }: HeroContentProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.h1
|
||||
id="hero-heading"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.1 }}
|
||||
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6 font-calligraphy"
|
||||
style={{
|
||||
fontWeight: 'normal',
|
||||
@@ -87,11 +92,13 @@ export function HeroDescription(_props: HeroContentProps) {
|
||||
}
|
||||
|
||||
export function HeroButtons({ isVisible }: HeroContentProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.3 }}
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8"
|
||||
>
|
||||
<MagneticButton strength={0.4}>
|
||||
@@ -118,20 +125,22 @@ export function HeroButtons({ isVisible }: HeroContentProps) {
|
||||
}
|
||||
|
||||
export function HeroFeatures({ isVisible }: HeroContentProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.35 }}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.35 }}
|
||||
className="flex flex-wrap gap-4 justify-center mb-16"
|
||||
>
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0.9 }}
|
||||
animate={isVisible ? { opacity: 1, scale: 1 } : {}}
|
||||
transition={{ duration: 0.4, delay: 0.4 + index * 0.1 }}
|
||||
whileHover={{ scale: 1.05, y: -2 }}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.4, delay: 0.4 + index * 0.1 }}
|
||||
whileHover={shouldReduceMotion ? {} : { scale: 1.05, y: -2 }}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#FAFAFA] border border-[#E5E5E5] transition-all duration-300 hover:border-[#1C1C1C] hover:shadow-md cursor-default"
|
||||
>
|
||||
<feature.icon className="w-4 h-4 text-[#C41E3A]" />
|
||||
@@ -144,6 +153,7 @@ export function HeroFeatures({ isVisible }: HeroContentProps) {
|
||||
|
||||
export function HeroStats() {
|
||||
const [statsVisible, setStatsVisible] = useState(false);
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
useEffect(() => {
|
||||
const statsEl = document.getElementById('stats-section');
|
||||
@@ -165,9 +175,9 @@ export function HeroStats() {
|
||||
return (
|
||||
<motion.div
|
||||
id="stats-section"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
animate={statsVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6, delay: 0.4 }}
|
||||
className="pt-16 border-t border-[#E2E8F0]"
|
||||
>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
|
||||
@@ -177,6 +187,7 @@ export function HeroStats() {
|
||||
stat={stat}
|
||||
index={index}
|
||||
shouldAnimate={statsVisible}
|
||||
shouldReduceMotion={shouldReduceMotion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -184,10 +195,11 @@ export function HeroStats() {
|
||||
);
|
||||
}
|
||||
|
||||
function HeroStatItem({ stat, index, shouldAnimate }: {
|
||||
function HeroStatItem({ stat, index, shouldAnimate, shouldReduceMotion }: {
|
||||
stat: { value: string; label: string };
|
||||
index: number;
|
||||
shouldAnimate: boolean;
|
||||
shouldReduceMotion: boolean;
|
||||
}) {
|
||||
const numericValue = parseInt(stat.value.replace(/\D/g, ''));
|
||||
const suffix = stat.value.replace(/[\d]/g, '');
|
||||
@@ -195,10 +207,10 @@ function HeroStatItem({ stat, index, shouldAnimate }: {
|
||||
return (
|
||||
<motion.div
|
||||
className="group cursor-default text-center"
|
||||
initial={{ opacity: 0, y: 20, scale: 0.9 }}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20, scale: 0.9 }}
|
||||
animate={shouldAnimate ? { opacity: 1, y: 0, scale: 1 } : {}}
|
||||
transition={{ duration: 0.5, delay: index * 0.1, type: 'spring', stiffness: 100 }}
|
||||
whileHover={{ scale: 1.05, y: -5 }}
|
||||
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.5, delay: index * 0.1, type: 'spring', stiffness: 100 }}
|
||||
whileHover={shouldReduceMotion ? {} : { scale: 1.05, y: -5 }}
|
||||
>
|
||||
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
|
||||
{shouldAnimate ? (
|
||||
|
||||
@@ -112,13 +112,13 @@ export function NewsSection({ config }: NewsSectionProps) {
|
||||
<CardDescription className="text-base leading-relaxed mb-6 flex-1">
|
||||
{newsItem.excerpt}
|
||||
</CardDescription>
|
||||
<a
|
||||
<Link
|
||||
href={`/news/${newsItem.id}`}
|
||||
className="inline-flex items-center text-sm font-medium text-[#1C1C1C] hover:text-[#C41E3A] transition-colors group/link"
|
||||
>
|
||||
阅读更多
|
||||
<ArrowRight className="ml-1 w-4 h-4 transition-transform group-hover/link:translate-x-1" />
|
||||
</a>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user