feat(e2e): 添加完整的E2E测试框架和测试用例

添加Playwright测试框架配置和基础页面对象
实现冒烟测试用例覆盖首页和联系页面核心功能
更新导航组件以支持滚动高亮功能
添加BackButton组件统一返回按钮行为
配置Woodpecker CI集成和测试报告生成
This commit is contained in:
张翔
2026-02-27 10:30:33 +08:00
parent 4a616fe96e
commit 5d5b7feb0a
50 changed files with 6765 additions and 46 deletions
+10 -7
View File
@@ -1,7 +1,7 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ArrowLeft, CheckCircle2, TrendingUp, Users, Target, Quote, Clock, MessageCircle, Award } from 'lucide-react';
@@ -31,6 +31,7 @@ interface CaseDetailClientProps {
export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
const [isVisible, setIsVisible] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const router = useRouter();
useEffect(() => {
const observer = new IntersectionObserver(
@@ -67,12 +68,14 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
<main className="min-h-screen bg-white">
<div className="relative overflow-hidden bg-gradient-to-b from-[#FAFAFA] to-white">
<div className="container-wide relative z-10 pt-32 pb-20">
<Link href="/cases">
<Button variant="ghost" className="text-[#5C5C5C] hover:text-[#C41E3A] hover:bg-[#C41E3A]/10">
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
</Link>
<Button
variant="ghost"
className="text-[#5C5C5C] hover:text-[#C41E3A] hover:bg-[#C41E3A]/10"
onClick={() => router.back()}
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
<div className="max-w-4xl mt-8">
<Badge className="mb-4 bg-[#C41E3A]/10 text-[#C41E3A] hover:bg-[#C41E3A]/20">
{caseItem.industry}
+10 -7
View File
@@ -1,7 +1,7 @@
'use client';
import { useRef } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
@@ -94,6 +94,7 @@ const outcomes = {
};
export function ServiceDetailClient({ service }: ServiceDetailClientProps) {
const router = useRouter();
const contentRef = useRef<HTMLDivElement>(null);
const serviceChallenges = challenges[service.id as keyof typeof challenges] || [];
@@ -106,12 +107,14 @@ export function ServiceDetailClient({ service }: ServiceDetailClientProps) {
<main className="min-h-screen bg-white">
<div className="relative overflow-hidden bg-gradient-to-b from-[#FAFAFA] to-white">
<div className="container-wide relative z-10 pt-32 pb-20">
<Link href="/services">
<Button variant="ghost" className="text-[#5C5C5C] hover:text-[#C41E3A] hover:bg-[#C41E3A]/10">
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
</Link>
<Button
variant="ghost"
className="text-[#5C5C5C] hover:text-[#C41E3A] hover:bg-[#C41E3A]/10"
onClick={() => router.back()}
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
<div className="max-w-4xl mt-8">
<div className="flex items-center gap-4 mb-6">
<div className="w-16 h-16 bg-[#C41E3A] rounded-2xl flex items-center justify-center text-white">
+8 -10
View File
@@ -3,7 +3,7 @@
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ArrowLeft, Calendar, Share2 } from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useRef } from 'react';
@@ -18,13 +18,10 @@ interface NewsItem {
content: string;
}
interface NewsDetailClientProps {
news: NewsItem;
}
export function NewsDetailClient({ news }: NewsDetailClientProps) {
const contentRef = useRef(null);
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
const router = useRouter();
const relatedNews = NEWS
.filter((n) => n.id !== news.id && n.category === news.category)
@@ -34,13 +31,14 @@ export function NewsDetailClient({ news }: NewsDetailClientProps) {
<div className="min-h-screen bg-white">
<div className="relative overflow-hidden bg-gradient-to-b from-[#FAFAFA] to-white">
<div className="container-wide relative z-10 pt-32 pb-20">
<Link
href="/news"
className="inline-flex items-center text-[#5C5C5C] hover:text-[#C41E3A] transition-colors mb-6"
<Button
variant="ghost"
className="text-[#5C5C5C] hover:text-[#C41E3A] hover:bg-[#C41E3A]/10"
onClick={() => router.back()}
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Link>
</Button>
<div className="max-w-4xl">
<div className="inline-block px-4 py-2 bg-[#C41E3A]/10 rounded-full text-[#C41E3A] text-sm mb-6">
{news.category}
+3 -8
View File
@@ -1,8 +1,9 @@
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { PRODUCTS } from '@/lib/constants';
import { Button } from '@/components/ui/button';
import { BackButton } from '@/components/ui/back-button';
import { ArrowLeft, CheckCircle2, Zap, Target, Layers, CreditCard, ArrowRight } from 'lucide-react';
import Link from 'next/link';
export async function generateStaticParams() {
return PRODUCTS.map((product) => ({
@@ -38,13 +39,7 @@ export default async function ProductDetailPage({ params }: { params: Promise<{
<div className="min-h-screen bg-white">
<div className="relative overflow-hidden bg-gradient-to-b from-[#FAFAFA] to-white">
<div className="container-wide relative z-10 pt-32 pb-20">
<Link
href="/#products"
className="inline-flex items-center text-[#5C5C5C] hover:text-[#C41E3A] transition-colors mb-6"
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Link>
<BackButton />
<div className="max-w-4xl">
<div className="inline-block px-4 py-2 bg-[#C41E3A]/10 rounded-full text-[#C41E3A] text-sm mb-6">
{product.category}
+31 -5
View File
@@ -12,12 +12,29 @@ import { useFocusTrap } from '@/hooks/use-focus-trap';
export function Header() {
const [isOpen, setIsOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const [activeSection, setActiveSection] = useState('home');
const pathname = usePathname();
const focusTrapRef = useFocusTrap<HTMLDivElement>(isOpen);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
if (pathname === '/') {
const sections = ['home', 'services', 'products', 'cases', 'about', 'news', 'contact'];
const scrollPosition = window.scrollY + 100;
for (const sectionId of sections) {
const element = document.getElementById(sectionId);
if (element) {
const { offsetTop, offsetHeight } = element;
if (scrollPosition >= offsetTop && scrollPosition < offsetTop + offsetHeight) {
setActiveSection(sectionId);
break;
}
}
}
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
@@ -26,7 +43,7 @@ export function Header() {
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
}, [pathname]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -38,6 +55,15 @@ export function Header() {
}
}, [isOpen]);
const isActive = (item: typeof NAVIGATION[number]) => {
if (pathname === '/') {
return activeSection === item.id;
}
const navPath = item.href.split('#')[0];
return pathname === navPath || pathname.startsWith(navPath + '/');
};
return (
<>
<header
@@ -72,15 +98,15 @@ export function Header() {
relative px-3 py-1.5 text-sm font-medium
transition-all duration-300
focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-offset-2 rounded-sm
${pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href))
${isActive(item)
? 'text-[#1C1C1C]'
: 'text-[#3D3D3D] hover:text-[#1C1C1C]'
}
`}
aria-current={pathname === item.href ? 'page' : undefined}
aria-current={isActive(item) ? 'page' : undefined}
>
{item.label}
{(pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href))) && (
{isActive(item) && (
<motion.span
layoutId="activeNav"
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-6 h-0.5 bg-[#C41E3A] rounded-full"
@@ -154,7 +180,7 @@ export function Header() {
transition-all duration-300
border-l-2
focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-inset
${pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href))
${isActive(item)
? 'text-[#1C1C1C] border-[#C41E3A] bg-[#FEF2F4]'
: 'text-[#3D3D3D] border-transparent hover:text-[#1C1C1C] hover:bg-[#F5F5F5]'
}
+2 -2
View File
@@ -21,7 +21,7 @@ export function ServicesSection() {
const isInView = useInView(ref, { once: true, margin: '-100px' });
return (
<section id="solutions" aria-labelledby="solutions-heading" className="py-24 bg-white relative overflow-hidden" ref={ref}>
<section id="services" aria-labelledby="services-heading" className="py-24 bg-white relative overflow-hidden" ref={ref}>
<div className="absolute top-1/3 left-0 w-[400px] h-[400px] bg-[rgba(196,30,58,0.03)] rounded-full blur-3xl" />
<div className="absolute top-1/3 right-0 w-[300px] h-[300px] bg-[rgba(196,30,58,0.02)] rounded-full blur-3xl" />
@@ -32,7 +32,7 @@ export function ServicesSection() {
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto mb-16"
>
<h2 id="solutions-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-4">
<h2 id="services-heading" className="text-4xl md:text-5xl font-bold text-[#1C1C1C] mb-4">
<span className="text-[#C41E3A]"></span>
</h2>
<p className="text-lg text-[#5C5C5C] max-w-2xl mx-auto">
+20
View File
@@ -0,0 +1,20 @@
'use client';
import { useRouter } from 'next/navigation';
import { ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
export function BackButton() {
const router = useRouter();
return (
<Button
variant="ghost"
className="text-[#5C5C5C] hover:text-[#C41E3A] hover:bg-[#C41E3A]/10"
onClick={() => router.back()}
>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
);
}
+8 -7
View File
@@ -11,14 +11,15 @@ export const COMPANY_INFO = {
address: '中国四川省成都市龙泉驿区幸福路12号',
} as const;
// Navigation Items - 独立页面导航
// Navigation Items - 混合导航(首页滚动,详情页跳转)
export const NAVIGATION = [
{ id: 'home', label: '首页', href: '/' },
{ id: 'services', label: '核心业务', href: '/services' },
{ id: 'products', label: '产品服务', href: '/products' },
{ id: 'about', label: '关于我们', href: '/about' },
{ id: 'news', label: '新闻动态', href: '/news' },
{ id: 'contact', label: '联系我们', href: '/contact' },
{ id: 'home', label: '首页', href: '/#home' },
{ id: 'services', label: '核心业务', href: '/#services' },
{ id: 'products', label: '产品服务', href: '/#products' },
{ id: 'cases', label: '成功案例', href: '/#cases' },
{ id: 'about', label: '关于我们', href: '/#about' },
{ id: 'news', label: '新闻动态', href: '/#news' },
{ id: 'contact', label: '联系我们', href: '/#contact' },
] as const;
// Stats Data