feat(e2e): 添加完整的E2E测试框架和测试用例
添加Playwright测试框架配置和基础页面对象 实现冒烟测试用例覆盖首页和联系页面核心功能 更新导航组件以支持滚动高亮功能 添加BackButton组件统一返回按钮行为 配置Woodpecker CI集成和测试报告生成
This commit is contained in:
@@ -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]'
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user