diff --git a/.gitignore b/.gitignore
index 0e12435..808d909 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,43 @@ build/
dist/
.cache/
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+.pytest_cache/
+.mypy_cache/
+.coverage
+htmlcov/
+.tox/
+.hypothesis/
+.pytest_cache/
+.coverage.*
+*.cover
+
+# Playwright
+test-results/
+playwright-report/
+playwright/.cache/
+
# Testing
coverage/
.nyc_output/
diff --git a/.woodpecker.yml b/.woodpecker.yml
new file mode 100644
index 0000000..e1741ef
--- /dev/null
+++ b/.woodpecker.yml
@@ -0,0 +1,148 @@
+pipeline:
+ e2e-tests:
+ image: node:18-alpine
+ environment:
+ NODE_ENV: test
+ CI: true
+ commands:
+ - cd e2e
+ - npm ci
+ - npx playwright install --with-deps chromium
+ - npx playwright install --with-deps firefox
+ - npx playwright install --with-deps webkit
+ - npm run test:ci
+ when:
+ event:
+ - push
+ - pull_request
+ branch:
+ - main
+ - develop
+
+ e2e-tests-smoke:
+ image: node:18-alpine
+ environment:
+ NODE_ENV: test
+ CI: true
+ commands:
+ - cd e2e
+ - npm ci
+ - npx playwright install --with-deps chromium
+ - npm run test:smoke
+ when:
+ event:
+ - push
+ - pull_request
+
+ e2e-tests-regression:
+ image: node:18-alpine
+ environment:
+ NODE_ENV: test
+ CI: true
+ commands:
+ - cd e2e
+ - npm ci
+ - npx playwright install --with-deps chromium
+ - npm run test:regression
+ when:
+ event:
+ - push
+ branch:
+ - main
+
+ e2e-tests-performance:
+ image: node:18-alpine
+ environment:
+ NODE_ENV: test
+ CI: true
+ commands:
+ - cd e2e
+ - npm ci
+ - npx playwright install --with-deps chromium
+ - npm run test:performance
+ when:
+ event:
+ - push
+ branch:
+ - main
+
+ e2e-tests-responsive:
+ image: node:18-alpine
+ environment:
+ NODE_ENV: test
+ CI: true
+ commands:
+ - cd e2e
+ - npm ci
+ - npx playwright install --with-deps chromium
+ - npm run test:responsive
+ when:
+ event:
+ - push
+ branch:
+ - main
+
+ e2e-tests-visual:
+ image: node:18-alpine
+ environment:
+ NODE_ENV: test
+ CI: true
+ commands:
+ - cd e2e
+ - npm ci
+ - npx playwright install --with-deps chromium
+ - npm run test:visual
+ when:
+ event:
+ - push
+ branch:
+ - main
+
+ e2e-tests-a11y:
+ image: node:18-alpine
+ environment:
+ NODE_ENV: test
+ CI: true
+ commands:
+ - cd e2e
+ - npm ci
+ - npx playwright install --with-deps chromium
+ - npm run test:a11y
+ when:
+ event:
+ - push
+ branch:
+ - main
+
+ e2e-tests-report:
+ image: node:18-alpine
+ environment:
+ NODE_ENV: test
+ CI: true
+ commands:
+ - cd e2e
+ - npm ci
+ - npx playwright install --with-deps chromium
+ - npm run test:report
+ when:
+ event:
+ - push
+ branch:
+ - main
+
+ e2e-tests-all-browsers:
+ image: node:18-alpine
+ environment:
+ NODE_ENV: test
+ CI: true
+ commands:
+ - cd e2e
+ - npm ci
+ - npx playwright install --with-deps chromium firefox webkit
+ - npm run test:all-browsers
+ when:
+ event:
+ - push
+ branch:
+ - main
+ - develop
diff --git a/docs/plans/2026-02-26-e2e-test-framework-implementation.md b/docs/plans/2026-02-26-e2e-test-framework-implementation.md
new file mode 100644
index 0000000..7a62fcd
--- /dev/null
+++ b/docs/plans/2026-02-26-e2e-test-framework-implementation.md
@@ -0,0 +1,200 @@
+# TypeScript + Playwright E2E 测试框架实施计划
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**目标:** 为 Novalon Website 构建一个完整的、类型安全的 E2E 测试框架,使用 Playwright + TypeScript,实现全面的功能、性能、响应式和视觉测试覆盖,并集成 Woodpecker CI 自动化流程。
+
+**架构:** 采用页面对象模式 (POM) 设计,所有测试代码使用 TypeScript 编写以获得类型安全,与 Next.js 项目共享类型定义。测试框架分为页面对象层、测试用例层、工具层和配置层,支持并行执行和多浏览器测试。
+
+**技术栈:** Playwright (TypeScript), Vitest/Playwright Test, TypeScript 5, Woodpecker CI, @axe-core/playwright (可访问性测试)
+
+---
+
+## 阶段 1: 基础框架搭建
+
+### Task 1: 初始化 Playwright 项目
+
+**Files:**
+- Create: `e2e/package.json`
+- Create: `e2e/tsconfig.json`
+- Create: `e2e/playwright.config.ts`
+- Create: `e2e/.env.example`
+
+**Step 1: 创建 e2e/package.json**
+
+```json
+{
+ "name": "e2e-tests",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "test": "playwright test",
+ "test:ui": "playwright test --ui",
+ "test:debug": "playwright test --debug",
+ "test:headed": "playwright test --headed",
+ "test:smoke": "playwright test --grep @smoke",
+ "test:regression": "playwright test --grep @regression",
+ "test:performance": "playwright test --grep @performance",
+ "test:responsive": "playwright test --grep @responsive",
+ "test:visual": "playwright test --grep @visual",
+ "test:report": "playwright show-report",
+ "install": "playwright install --with-deps"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.48.0",
+ "@axe-core/playwright": "^4.9.0",
+ "@types/node": "^20.11.0",
+ "typescript": "^5.3.0"
+ }
+}
+```
+
+**Step 2: 创建 e2e/tsconfig.json**
+
+```json
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "commonjs",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "jsx": "react",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "baseUrl": "..",
+ "paths": {
+ "@/*": ["src/*"],
+ "@e2e/*": ["e2e/*"]
+ }
+ },
+ "include": ["e2e/**/*"],
+ "exclude": ["node_modules", "dist", ".next"]
+}
+```
+
+**Step 3: 创建 e2e/playwright.config.ts**
+
+```typescript
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './tests',
+ timeout: 30 * 1000,
+ expect: {
+ timeout: 5000
+ },
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 2 : 4,
+ reporter: [
+ ['html', { outputFolder: 'playwright-report', open: 'never' }],
+ ['json', { outputFile: 'test-results.json' }],
+ ['junit', { outputFile: 'test-results.xml' }],
+ ['list']
+ ],
+ use: {
+ baseURL: process.env.BASE_URL || 'http://localhost:3000',
+ trace: 'on-first-retry',
+ screenshot: 'only-on-failure',
+ video: 'retain-on-failure',
+ viewport: { width: 1920, height: 1080 },
+ ignoreHTTPSErrors: true,
+ contextOptions: {
+ locale: 'zh-CN',
+ timezoneId: 'Asia/Shanghai'
+ }
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ }
+ ],
+ webServer: {
+ command: 'npm run dev',
+ url: 'http://localhost:3000',
+ reuseExistingServer: !process.env.CI,
+ timeout: 120 * 1000,
+ },
+});
+```
+
+**Step 4: 创建 e2e/.env.example**
+
+```bash
+BASE_URL=http://localhost:3000
+TEST_ENV=development
+HEADLESS=true
+PARALLEL_WORKERS=4
+SCREENSHOT_ON_FAILURE=true
+VIDEO_ON_FAILURE=true
+```
+
+**Step 5: 安装依赖**
+
+Run: `cd e2e && npm install`
+
+Expected: 依赖安装成功,node_modules 目录创建
+
+**Step 6: 安装 Playwright 浏览器**
+
+Run: `cd e2e && npx playwright install --with-deps`
+
+Expected: 浏览器安装成功
+
+**Step 7: 提交**
+
+```bash
+git add e2e/package.json e2e/tsconfig.json e2e/playwright.config.ts e2e/.env.example
+git commit -m "feat: initialize Playwright TypeScript E2E test framework"
+```
+
+---
+
+## 总结
+
+实施计划已创建完成,包含以下阶段:
+
+1. **阶段 1: 基础框架搭建** - 6 个任务
+2. **阶段 2: 核心页面对象实现** - 2 个任务
+3. **阶段 3: 冒烟测试实现** - 3 个任务
+4. **阶段 4: 回归测试实现** - 2 个任务
+5. **阶段 5: 性能测试实现** - 3 个任务
+6. **阶段 6: 响应式测试实现** - 3 个任务
+7. **阶段 7: 视觉回归测试** - 2 个任务
+8. **阶段 8: Woodpecker CI 集成** - 1 个任务
+
+总计:22 个任务,预计 8-10 周完成。
+
+每个任务都包含:
+- 详细的代码实现
+- 运行验证步骤
+- 预期结果
+- Git 提交命令
+
+---
+
+## 下一步
+
+**Plan complete and saved to `docs/plans/2026-02-26-e2e-test-framework-implementation.md`.**
+
+**Two execution options:**
+
+**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
+
+**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
+
+**Which approach?**
diff --git a/docs/plans/2026-02-27-e2e-driven-system-optimization.md b/docs/plans/2026-02-27-e2e-driven-system-optimization.md
new file mode 100644
index 0000000..00791ac
--- /dev/null
+++ b/docs/plans/2026-02-27-e2e-driven-system-optimization.md
@@ -0,0 +1,885 @@
+# E2E测试结果驱动的系统优化实施计划
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**目标:** 基于E2E测试结果,通过TDD方法系统性地修复测试失败问题,提升系统质量
+
+**架构:** 采用测试驱动开发(TDD)方法,按照优先级逐步修复问题:优先级1(关键功能)→ 优先级2(性能优化)→ 优先级3(响应式改进)
+
+**技术栈:** Next.js 15, React, TypeScript, Playwright E2E测试, Tailwind CSS
+
+---
+
+## 阶段1: 优先级1 - 关键功能修复
+
+### Task 1: 完善联系表单提交功能
+
+**背景:** 回归测试显示联系表单提交功能未完全实现,导致测试失败
+
+**Files:**
+- Create: `src/app/contact/actions.ts`
+- Modify: `src/app/contact/page.tsx`
+- Test: `e2e/src/tests/regression/contact-form.regression.spec.ts`
+
+**Step 1: 编写失败的测试**
+
+```typescript
+// e2e/src/tests/regression/contact-form.regression.spec.ts
+test('应该能够提交完整的表单', async ({ contactPage }) => {
+ await contactPage.fillContactForm({
+ name: '测试用户',
+ email: 'test@example.com',
+ phone: '13800138000',
+ message: '这是一条测试消息'
+ });
+
+ await contactPage.submitForm();
+
+ await contactPage.page.waitForTimeout(2000);
+
+ const successMessage = await contactPage.page.locator('text=提交成功').isVisible();
+ expect(successMessage).toBeTruthy();
+});
+```
+
+**Step 2: 运行测试验证失败**
+
+Run: `cd e2e && npm run test:regression -- --grep "应该能够提交完整的表单"`
+
+Expected: FAIL - 表单提交功能未实现
+
+**Step 3: 创建表单提交Action**
+
+```typescript
+// src/app/contact/actions.ts
+'use server';
+
+export async function submitContactForm(formData: FormData) {
+ const name = formData.get('name') as string;
+ const email = formData.get('email') as string;
+ const phone = formData.get('phone') as string;
+ const message = formData.get('message') as string;
+
+ if (!name || !email || !message) {
+ return { success: false, error: '请填写必填字段' };
+ }
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ return { success: false, error: '请输入有效的邮箱地址' };
+ }
+
+ return { success: true, message: '提交成功' };
+}
+```
+
+**Step 4: 更新联系页面集成表单提交**
+
+```typescript
+// src/app/contact/page.tsx
+'use client';
+
+import { useState } from 'react';
+import { submitContactForm } from './actions';
+
+export default function ContactPage() {
+ const [formData, setFormData] = useState({
+ name: '',
+ email: '',
+ phone: '',
+ message: ''
+ });
+ const [submitResult, setSubmitResult] = useState<{ success: boolean; message?: string } | null>(null);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ const formDataObj = new FormData();
+ Object.entries(formData).forEach(([key, value]) => {
+ formDataObj.append(key, value);
+ });
+
+ const result = await submitContactForm(formDataObj);
+ setSubmitResult(result);
+ };
+
+ return (
+
+ );
+}
+```
+
+**Step 5: 运行测试验证通过**
+
+Run: `cd e2e && npm run test:regression -- --grep "应该能够提交完整的表单"`
+
+Expected: PASS
+
+**Step 6: 提交**
+
+```bash
+git add src/app/contact/actions.ts src/app/contact/page.tsx
+git commit -m "feat: implement contact form submission with validation"
+```
+
+---
+
+### Task 2: 优化移动端菜单交互
+
+**背景:** 回归测试显示移动端菜单交互需要完善
+
+**Files:**
+- Modify: `src/components/Header.tsx`
+- Test: `e2e/src/tests/regression/navigation.regression.spec.ts`
+
+**Step 1: 编写失败的测试**
+
+```typescript
+// e2e/src/tests/regression/navigation.regression.spec.ts
+test('应该能够打开和关闭移动端菜单', async ({ homePage }) => {
+ await homePage.page.setViewportSize({ width: 375, height: 667 });
+
+ await homePage.openMobileMenu();
+ const isMenuOpen = await homePage.isMobileMenuOpen();
+ expect(isMenuOpen).toBeTruthy();
+
+ await homePage.closeMobileMenu();
+ const isMenuClosed = await !(await homePage.isMobileMenuOpen());
+ expect(isMenuClosed).toBeTruthy();
+});
+```
+
+**Step 2: 运行测试验证失败**
+
+Run: `cd e2e && npm run test:regression -- --grep "应该能够打开和关闭移动端菜单"`
+
+Expected: FAIL - 移动端菜单交互未实现
+
+**Step 3: 实现移动端菜单状态管理**
+
+```typescript
+// src/components/Header.tsx
+'use client';
+
+import { useState } from 'react';
+
+export default function Header() {
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+
+ const toggleMobileMenu = () => {
+ setIsMobileMenuOpen(!isMobileMenuOpen);
+ };
+
+ return (
+
+ );
+}
+```
+
+**Step 4: 运行测试验证通过**
+
+Run: `cd e2e && npm run test:regression -- --grep "应该能够打开和关闭移动端菜单"`
+
+Expected: PASS
+
+**Step 5: 提交**
+
+```bash
+git add src/components/Header.tsx
+git commit -m "feat: implement mobile menu toggle functionality"
+```
+
+---
+
+### Task 3: 修复页面滚动问题
+
+**背景:** 回归测试显示页面滚动事件处理需要优化
+
+**Files:**
+- Create: `src/hooks/useSmoothScroll.ts`
+- Modify: `src/components/Header.tsx`, `src/app/contact/page.tsx`
+- Test: `e2e/src/tests/regression/navigation.regression.spec.ts`
+
+**Step 1: 编写失败的测试**
+
+```typescript
+// e2e/src/tests/regression/navigation.regression.spec.ts
+test('应该能够平滑滚动到锚点', async ({ homePage }) => {
+ await homePage.navigateTo('/');
+
+ await homePage.scrollToSection('services');
+ const servicesSection = homePage.page.locator('#services');
+ const isVisible = await servicesSection.isVisible();
+ expect(isVisible).toBeTruthy();
+});
+```
+
+**Step 2: 运行测试验证失败**
+
+Run: `cd e2e && npm run test:regression -- --grep "应该能够平滑滚动到锚点"`
+
+Expected: FAIL - 平滑滚动未实现
+
+**Step 3: 创建平滑滚动Hook**
+
+```typescript
+// src/hooks/useSmoothScroll.ts
+import { useEffect } from 'react';
+
+export function useSmoothScroll() {
+ useEffect(() => {
+ const handleAnchorClick = (e: MouseEvent) => {
+ const target = e.target as HTMLAnchorElement;
+ if (target.tagName === 'A' && target.hash) {
+ e.preventDefault();
+ const element = document.querySelector(target.hash);
+ if (element) {
+ element.scrollIntoView({ behavior: 'smooth' });
+ }
+ }
+ };
+
+ document.addEventListener('click', handleAnchorClick);
+ return () => document.removeEventListener('click', handleAnchorClick);
+ }, []);
+}
+```
+
+**Step 4: 集成平滑滚动到页面**
+
+```typescript
+// src/app/contact/page.tsx
+import { useSmoothScroll } from '@/hooks/useSmoothScroll';
+
+export default function ContactPage() {
+ useSmoothScroll();
+
+ return (
+
+ );
+}
+```
+
+**Step 5: 运行测试验证通过**
+
+Run: `cd e2e && npm run test:regression -- --grep "应该能够平滑滚动到锚点"`
+
+Expected: PASS
+
+**Step 6: 提交**
+
+```bash
+git add src/hooks/useSmoothScroll.ts src/app/contact/page.tsx
+git commit -m "feat: implement smooth scrolling for anchor links"
+```
+
+---
+
+## 阶段2: 优先级2 - 性能优化
+
+### Task 4: 优化页面加载时间
+
+**背景:** 性能测试显示页面加载时间超出阈值
+
+**Files:**
+- Modify: `next.config.mjs`
+- Create: `src/app/loading.tsx`
+- Test: `e2e/src/tests/performance/performance.spec.ts`
+
+**Step 1: 编写失败的测试**
+
+```typescript
+// e2e/src/tests/performance/performance.spec.ts
+test('首页应该在2秒内完成加载', async ({ page }) => {
+ const startTime = Date.now();
+ await page.goto('/');
+ await page.waitForLoadState('networkidle');
+ const loadTime = Date.now() - startTime;
+
+ expect(loadTime).toBeLessThan(2000);
+});
+```
+
+**Step 2: 运行测试验证失败**
+
+Run: `cd e2e && npm run test:performance -- --grep "首页应该在2秒内完成加载"`
+
+Expected: FAIL - 页面加载时间超过2秒
+
+**Step 3: 配置代码分割和优化**
+
+```javascript
+// next.config.mjs
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+ swcMinify: true,
+ compress: true,
+ images: {
+ formats: ['image/avif', 'image/webp'],
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920],
+ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
+ },
+ experimental: {
+ optimizeCss: true,
+ },
+};
+
+export default nextConfig;
+```
+
+**Step 4: 创建加载状态组件**
+
+```typescript
+// src/app/loading.tsx
+export default function Loading() {
+ return (
+
+ );
+}
+```
+
+**Step 5: 运行测试验证通过**
+
+Run: `cd e2e && npm run test:performance -- --grep "首页应该在2秒内完成加载"`
+
+Expected: PASS
+
+**Step 6: 提交**
+
+```bash
+git add next.config.mjs src/app/loading.tsx
+git commit -m "perf: optimize page load time with code splitting and image optimization"
+```
+
+---
+
+### Task 5: 改善交互响应时间
+
+**背景:** 性能测试显示交互响应时间需要优化
+
+**Files:**
+- Modify: `src/components/Header.tsx`, `src/components/Button.tsx`
+- Test: `e2e/src/tests/performance/performance.spec.ts`
+
+**Step 1: 编写失败的测试**
+
+```typescript
+// e2e/src/tests/performance/performance.spec.ts
+test('按钮点击应该在100ms内响应', async ({ page }) => {
+ await page.goto('/');
+ const button = page.locator('button:has-text("立即咨询")').first();
+
+ const startTime = Date.now();
+ await button.click();
+ const responseTime = Date.now() - startTime;
+
+ expect(responseTime).toBeLessThan(100);
+});
+```
+
+**Step 2: 运行测试验证失败**
+
+Run: `cd e2e && npm run test:performance -- --grep "按钮点击应该在100ms内响应"`
+
+Expected: FAIL - 按钮响应时间超过100ms
+
+**Step 3: 优化按钮组件**
+
+```typescript
+// src/components/Button.tsx
+'use client';
+
+import { forwardRef } from 'react';
+
+export const Button = forwardRef>(
+ ({ children, onClick, ...props }, ref) => {
+ const handleClick = (e: React.MouseEvent) => {
+ requestAnimationFrame(() => {
+ onClick?.(e);
+ });
+ };
+
+ return (
+
+ );
+ }
+);
+
+Button.displayName = 'Button';
+```
+
+**Step 4: 运行测试验证通过**
+
+Run: `cd e2e && npm run test:performance -- --grep "按钮点击应该在100ms内响应"`
+
+Expected: PASS
+
+**Step 5: 提交**
+
+```bash
+git add src/components/Button.tsx
+git commit -m "perf: improve button click response time with requestAnimationFrame"
+```
+
+---
+
+### Task 6: 优化滚动性能
+
+**背景:** 性能测试显示滚动性能需要改善
+
+**Files:**
+- Create: `src/hooks/useThrottle.ts`
+- Modify: `src/components/Header.tsx`
+- Test: `e2e/src/tests/performance/performance.spec.ts`
+
+**Step 1: 编写失败的测试**
+
+```typescript
+// e2e/src/tests/performance/performance.spec.ts
+test('滚动帧率应该保持在60fps', async ({ page }) => {
+ await page.goto('/');
+
+ const frameRates: number[] = [];
+
+ for (let i = 0; i < 10; i++) {
+ await page.evaluate(() => {
+ window.scrollBy(0, 100);
+ });
+ await page.waitForTimeout(100);
+
+ const fps = await page.evaluate(() => {
+ return (window as any).performance?.fps || 60;
+ });
+ frameRates.push(fps);
+ }
+
+ const avgFps = frameRates.reduce((a, b) => a + b, 0) / frameRates.length;
+ expect(avgFps).toBeGreaterThan(55);
+});
+```
+
+**Step 2: 运行测试验证失败**
+
+Run: `cd e2e && npm run test:performance -- --grep "滚动帧率应该保持在60fps"`
+
+Expected: FAIL - 滚动帧率低于55fps
+
+**Step 3: 创建节流Hook**
+
+```typescript
+// src/hooks/useThrottle.ts
+import { useCallback, useRef } from 'react';
+
+export function useThrottle any>(
+ callback: T,
+ delay: number
+): T {
+ const lastRan = useRef(Date.now());
+
+ return useCallback(
+ (...args: Parameters) => {
+ if (Date.now() - lastRan.current >= delay) {
+ callback(...args);
+ lastRan.current = Date.now();
+ }
+ },
+ [callback, delay]
+ ) as T;
+}
+```
+
+**Step 4: 应用节流优化滚动事件**
+
+```typescript
+// src/components/Header.tsx
+import { useThrottle } from '@/hooks/useThrottle';
+
+export default function Header() {
+ const handleScroll = useThrottle(() => {
+ const isScrolled = window.scrollY > 50;
+ // 更新滚动状态
+ }, 100);
+
+ useEffect(() => {
+ window.addEventListener('scroll', handleScroll);
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, [handleScroll]);
+}
+```
+
+**Step 5: 运行测试验证通过**
+
+Run: `cd e2e && npm run test:performance -- --grep "滚动帧率应该保持在60fps"`
+
+Expected: PASS
+
+**Step 6: 提交**
+
+```bash
+git add src/hooks/useThrottle.ts src/components/Header.tsx
+git commit -m "perf: optimize scroll performance with throttle"
+```
+
+---
+
+## 阶段3: 优先级3 - 响应式改进
+
+### Task 7: 完善移动端布局
+
+**背景:** 响应式测试显示移动端布局需要完善
+
+**Files:**
+- Modify: `src/app/contact/page.tsx`, `src/components/Header.tsx`
+- Test: `e2e/src/tests/responsive/responsive.spec.ts`
+
+**Step 1: 编写失败的测试**
+
+```typescript
+// e2e/src/tests/responsive/responsive.spec.ts
+test('移动端布局应该正确显示', async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ await page.goto('/contact');
+
+ const form = page.locator('form');
+ const isVisible = await form.isVisible();
+
+ expect(isVisible).toBeTruthy();
+
+ const formWidth = await form.evaluate(el => el.getBoundingClientRect().width);
+ expect(formWidth).toBeLessThan(375);
+});
+```
+
+**Step 2: 运行测试验证失败**
+
+Run: `cd e2e && npm run test:responsive -- --grep "移动端布局应该正确显示"`
+
+Expected: FAIL - 移动端布局未优化
+
+**Step 3: 优化移动端表单布局**
+
+```typescript
+// src/app/contact/page.tsx
+export default function ContactPage() {
+ return (
+
+ );
+}
+```
+
+**Step 4: 运行测试验证通过**
+
+Run: `cd e2e && npm run test:responsive -- --grep "移动端布局应该正确显示"`
+
+Expected: PASS
+
+**Step 5: 提交**
+
+```bash
+git add src/app/contact/page.tsx
+git commit -m "style: improve mobile responsive layout for contact form"
+```
+
+---
+
+### Task 8: 优化平板端显示
+
+**背景:** 响应式测试显示平板端显示需要调整
+
+**Files:**
+- Modify: `src/app/contact/page.tsx`, `src/components/Header.tsx`
+- Test: `e2e/src/tests/responsive/responsive.spec.ts`
+
+**Step 1: 编写失败的测试**
+
+```typescript
+// e2e/src/tests/responsive/responsive.spec.ts
+test('平板端布局应该正确显示', async ({ page }) => {
+ await page.setViewportSize({ width: 768, height: 1024 });
+ await page.goto('/contact');
+
+ const form = page.locator('form');
+ const isVisible = await form.isVisible();
+
+ expect(isVisible).toBeTruthy();
+
+ const formWidth = await form.evaluate(el => el.getBoundingClientRect().width);
+ expect(formWidth).toBeGreaterThan(700);
+ expect(formWidth).toBeLessThan(768);
+});
+```
+
+**Step 2: 运行测试验证失败**
+
+Run: `cd e2e && npm run test:responsive -- --grep "平板端布局应该正确显示"`
+
+Expected: FAIL - 平板端布局未优化
+
+**Step 3: 优化平板端布局**
+
+```typescript
+// src/app/contact/page.tsx
+export default function ContactPage() {
+ return (
+
+
+
+ );
+}
+```
+
+**Step 4: 运行测试验证通过**
+
+Run: `cd e2e && npm run test:responsive -- --grep "平板端布局应该正确显示"`
+
+Expected: PASS
+
+**Step 5: 提交**
+
+```bash
+git add src/app/contact/page.tsx
+git commit -m "style: improve tablet responsive layout"
+```
+
+---
+
+### Task 9: 改善大屏幕体验
+
+**背景:** 响应式测试显示大屏幕显示需要优化
+
+**Files:**
+- Modify: `src/app/contact/page.tsx`, `src/app/page.tsx`
+- Test: `e2e/src/tests/responsive/responsive.spec.ts`
+
+**Step 1: 编写失败的测试**
+
+```typescript
+// e2e/src/tests/responsive/responsive.spec.ts
+test('大屏幕布局应该正确显示', async ({ page }) => {
+ await page.setViewportSize({ width: 1920, height: 1080 });
+ await page.goto('/contact');
+
+ const form = page.locator('form');
+ const isVisible = await form.isVisible();
+
+ expect(isVisible).toBeTruthy();
+
+ const formWidth = await form.evaluate(el => el.getBoundingClientRect().width);
+ expect(formWidth).toBeGreaterThan(1200);
+ expect(formWidth).toBeLessThan(1920);
+});
+```
+
+**Step 2: 运行测试验证失败**
+
+Run: `cd e2e && npm run test:responsive -- --grep "大屏幕布局应该正确显示"`
+
+Expected: FAIL - 大屏幕布局未优化
+
+**Step 3: 优化大屏幕布局**
+
+```typescript
+// src/app/contact/page.tsx
+export default function ContactPage() {
+ return (
+
+
+
+ );
+}
+```
+
+**Step 4: 运行测试验证通过**
+
+Run: `cd e2e && npm run test:responsive -- --grep "大屏幕布局应该正确显示"`
+
+Expected: PASS
+
+**Step 5: 提交**
+
+```bash
+git add src/app/contact/page.tsx
+git commit -m "style: improve large screen responsive layout"
+```
+
+---
+
+## 验证和总结
+
+### Task 10: 运行完整测试套件验证改进
+
+**Files:**
+- Test: `e2e/src/tests/**/*.spec.ts`
+
+**Step 1: 运行所有测试**
+
+Run: `cd e2e && npm run test:all-with-progress`
+
+Expected: 所有测试通过率显著提升
+
+**Step 2: 生成测试报告**
+
+Run: `cd e2e && npm run test:report`
+
+Expected: 生成详细的HTML测试报告
+
+**Step 3: 更新测试报告文档**
+
+Modify: `e2e/test-report.md`
+
+添加改进前后的对比数据和总结
+
+**Step 4: 提交**
+
+```bash
+git add e2e/test-report.md
+git commit -m "docs: update test report with improvements"
+```
+
+---
+
+## 执行策略
+
+### TDD流程
+每个任务都遵循以下TDD循环:
+1. **Red**: 编写失败的测试
+2. **Green**: 编写最小代码使测试通过
+3. **Refactor**: 重构代码(如果需要)
+4. **Commit**: 提交代码
+
+### 测试优先级
+- **冒烟测试**: 100%通过率(已达成)
+- **回归测试**: 目标80%+通过率
+- **性能测试**: 目标70%+通过率
+- **响应式测试**: 目标80%+通过率
+
+### 频繁提交
+每个任务完成后立即提交,保持代码历史清晰
+
+---
+
+## 成功标准
+
+- [ ] 回归测试通过率达到80%以上
+- [ ] 性能测试通过率达到70%以上
+- [ ] 响应式测试通过率达到80%以上
+- [ ] 所有冒烟测试保持100%通过
+- [ ] 页面加载时间优化到2秒以内
+- [ ] 移动端菜单交互流畅
+- [ ] 联系表单提交功能完整
+- [ ] 平滑滚动正常工作
+
+---
+
+## 风险和缓解
+
+### 风险1: 测试环境不稳定
+**缓解**: 使用Playwright的重试机制和超时配置
+
+### 风险2: 性能优化可能影响功能
+**缓解**: 每次优化后运行完整测试套件验证
+
+### 风险3: 响应式改动可能影响现有布局
+**缓解**: 逐步实施,每个改动后测试所有断点
+
+---
+
+## 时间估算
+
+- 阶段1(关键功能修复): 2-3小时
+- 阶段2(性能优化): 2-3小时
+- 阶段3(响应式改进): 1-2小时
+- 验证和总结: 1小时
+
+**总计**: 6-9小时
diff --git a/e2e/.env.example b/e2e/.env.example
new file mode 100644
index 0000000..2baf499
--- /dev/null
+++ b/e2e/.env.example
@@ -0,0 +1,2 @@
+BASE_URL=http://localhost:3000
+CI=false
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
new file mode 100644
index 0000000..e8b2f67
--- /dev/null
+++ b/e2e/package-lock.json
@@ -0,0 +1,231 @@
+{
+ "name": "e2e-tests",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "e2e-tests",
+ "version": "1.0.0",
+ "hasInstallScript": true,
+ "devDependencies": {
+ "@axe-core/playwright": "^4.9.0",
+ "@playwright/test": "^1.48.0",
+ "@types/node": "^20.11.0",
+ "glob": "^13.0.6",
+ "typescript": "^5.3.0"
+ }
+ },
+ "node_modules/@axe-core/playwright": {
+ "version": "4.11.1",
+ "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.1.tgz",
+ "integrity": "sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "axe-core": "~4.11.1"
+ },
+ "peerDependencies": {
+ "playwright-core": ">= 1.0.0"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
+ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.34",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.34.tgz",
+ "integrity": "sha512-by3/Z0Qp+L9cAySEsSNNwZ6WWw8ywgGLPQGgbQDhNRSitqYgkgp4pErd23ZSCavbtUA2CN4jQtoB3T8nk4j3Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.11.1",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz",
+ "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
+ "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
+ "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/e2e/package.json b/e2e/package.json
new file mode 100644
index 0000000..b9207c0
--- /dev/null
+++ b/e2e/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "e2e-tests",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "test": "playwright test",
+ "test:ui": "playwright test --ui",
+ "test:debug": "playwright test --debug",
+ "test:headed": "playwright test --headed",
+ "test:smoke": "playwright test --grep @smoke",
+ "test:regression": "playwright test --grep @regression",
+ "test:performance": "playwright test --grep @performance",
+ "test:responsive": "playwright test --grep @responsive",
+ "test:visual": "playwright test --grep @visual",
+ "test:report": "playwright show-report",
+ "test:all-with-progress": "node run-tests-with-progress.js",
+ "install": "playwright install --with-deps"
+ },
+ "devDependencies": {
+ "@axe-core/playwright": "^4.9.0",
+ "@playwright/test": "^1.48.0",
+ "@types/node": "^20.11.0",
+ "glob": "^13.0.6",
+ "typescript": "^5.3.0"
+ }
+}
diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts
new file mode 100644
index 0000000..84e984c
--- /dev/null
+++ b/e2e/playwright.config.ts
@@ -0,0 +1,53 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './src/tests',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: [
+ ['html', { open: 'never' }],
+ ['json', { outputFile: 'test-results/results.json' }],
+ ['junit', { outputFile: 'test-results/junit.xml' }],
+ ['line'],
+ ['list']
+ ],
+ timeout: 60000,
+ expect: {
+ timeout: 30000
+ },
+ use: {
+ baseURL: 'http://localhost:3001',
+ trace: 'retain-on-failure',
+ screenshot: 'only-on-failure',
+ video: 'retain-on-failure',
+ headless: true,
+ viewport: { width: 1280, height: 720 },
+ actionTimeout: 30000,
+ navigationTimeout: 60000
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+ {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] },
+ },
+ ],
+ webServer: undefined,
+});
diff --git a/e2e/run-tests-with-progress.js b/e2e/run-tests-with-progress.js
new file mode 100644
index 0000000..ab6f7b4
--- /dev/null
+++ b/e2e/run-tests-with-progress.js
@@ -0,0 +1,147 @@
+#!/usr/bin/env node
+
+const { spawn } = require('child_process');
+const fs = require('fs');
+const { glob } = require('glob');
+
+const testTypes = [
+ { name: '冒烟测试', script: 'test:smoke', pattern: 'src/tests/smoke/**/*.spec.ts' },
+ { name: '回归测试', script: 'test:regression', pattern: 'src/tests/regression/**/*.spec.ts' },
+ { name: '性能测试', script: 'test:performance', pattern: 'src/tests/performance/**/*.spec.ts' },
+ { name: '响应式测试', script: 'test:responsive', pattern: 'src/tests/responsive/**/*.spec.ts' }
+];
+
+const TIMEOUT_SECONDS = 600;
+
+async function runTests() {
+ console.log('🧪 开始运行E2E测试...\n');
+
+ const results = {
+ total: 0,
+ passed: 0,
+ failed: 0,
+ byType: {}
+ };
+
+ for (const testType of testTypes) {
+ console.log(`\n${'='.repeat(60)}`);
+ console.log(`📋 ${testType.name}`);
+ console.log(`${'='.repeat(60)}`);
+
+ await new Promise((resolve) => {
+ const startTime = Date.now();
+ let lastUpdateTime = startTime;
+ let currentTest = 0;
+ let passedCount = 0;
+ let failedCount = 0;
+ let isComplete = false;
+ let lastTestName = '';
+
+ const testProcess = spawn('npm', ['run', testType.script], {
+ cwd: __dirname,
+ shell: true,
+ stdio: ['pipe', 'pipe', 'pipe']
+ });
+
+ testProcess.stdout.on('data', (data) => {
+ const output = data.toString();
+
+ if (output.includes('›')) {
+ currentTest++;
+ const progress = Math.min(100, Math.round((currentTest / 100) * 100));
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
+ const barLength = Math.floor(progress / 2);
+ const bar = '█'.repeat(barLength) + '░'.repeat(50 - barLength);
+
+ const testNameMatch = output.match(/›\s+(.+)/);
+ if (testNameMatch) {
+ lastTestName = testNameMatch[1].trim();
+ }
+
+ process.stdout.write(`\r⏳ 进度: [${bar}] ${progress}% - ${elapsed}s - ${lastTestName}`);
+ lastUpdateTime = Date.now();
+ }
+
+ if (output.includes('passed')) {
+ const match = output.match(/(\d+)\s+passed/);
+ if (match) {
+ passedCount = parseInt(match[1]);
+ }
+ }
+
+ if (output.includes('failed')) {
+ const match = output.match(/(\d+)\s+failed/);
+ if (match) {
+ failedCount = parseInt(match[1]);
+ }
+ }
+ });
+
+ testProcess.stderr.on('data', (data) => {
+ const output = data.toString();
+ if (output.includes('Error') || output.includes('error')) {
+ process.stdout.write('\n❌ 错误: ' + output);
+ }
+ });
+
+ testProcess.on('close', (code) => {
+ isComplete = true;
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
+ process.stdout.write(`\r✅ 完成: [${'█'.repeat(50)}] 100% - ${elapsed}s\n`);
+
+ results.total += passedCount + failedCount;
+ results.passed += passedCount;
+ results.failed += failedCount;
+ results.byType[testType.name] = {
+ total: passedCount + failedCount,
+ passed: passedCount,
+ failed: failedCount,
+ elapsed: elapsed
+ };
+
+ resolve();
+ });
+
+ const progressInterval = setInterval(() => {
+ if (!isComplete) {
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
+ const timeSinceLastUpdate = Date.now() - lastUpdateTime;
+
+ if (timeSinceLastUpdate > 10000 && timeSinceLastUpdate < 30000) {
+ process.stdout.write(`\r⏳ 等待测试... (${elapsed}s) - ${lastTestName}`);
+ } else if (timeSinceLastUpdate >= 30000) {
+ process.stdout.write(`\r⚠️ 测试可能卡住 (${elapsed}s) - ${lastTestName}`);
+ }
+
+ if (elapsed > TIMEOUT_SECONDS) {
+ console.log(`\n❌ 测试超时 (${TIMEOUT_SECONDS}s),正在停止...`);
+ testProcess.kill();
+ clearInterval(progressInterval);
+ isComplete = true;
+ resolve();
+ }
+ }
+ }, 5000);
+
+ testProcess.on('close', () => {
+ clearInterval(progressInterval);
+ });
+ });
+ }
+
+ console.log(`\n${'='.repeat(60)}`);
+ console.log('📊 测试结果汇总');
+ console.log(`${'='.repeat(60)}`);
+ console.log(`总测试数: ${results.total}`);
+ console.log(`通过: ${results.passed} (${((results.passed / results.total) * 100).toFixed(1)}%)`);
+ console.log(`失败: ${results.failed} (${((results.failed / results.total) * 100).toFixed(1)}%)`);
+
+ console.log('\n分类结果:');
+ for (const [name, result] of Object.entries(results.byType)) {
+ const passRate = ((result.passed / result.total) * 100).toFixed(1);
+ const status = passRate >= 80 ? '✅' : passRate >= 50 ? '⚠️' : '❌';
+ console.log(` ${status} ${name}: ${result.passed}/${result.total} (${passRate}%) - ${result.elapsed}s`);
+ }
+}
+
+runTests().catch(console.error);
diff --git a/e2e/src/fixtures/a11y.fixture.ts b/e2e/src/fixtures/a11y.fixture.ts
new file mode 100644
index 0000000..f0e80f3
--- /dev/null
+++ b/e2e/src/fixtures/a11y.fixture.ts
@@ -0,0 +1,11 @@
+import { test as base } from '@playwright/test';
+import { AxeBuilder } from '@axe-core/playwright';
+
+export const test = base.extend({
+ makeAxeBuilder: async ({ page }, use) => {
+ const makeAxeBuilder = () => new AxeBuilder({ page });
+ await use(makeAxeBuilder);
+ },
+});
+
+export { expect } from '@playwright/test';
diff --git a/e2e/src/fixtures/base.fixture.ts b/e2e/src/fixtures/base.fixture.ts
new file mode 100644
index 0000000..96f3a82
--- /dev/null
+++ b/e2e/src/fixtures/base.fixture.ts
@@ -0,0 +1,28 @@
+import { test as base, Page } from '@playwright/test';
+import { HomePage } from '../pages/HomePage';
+import { ContactPage } from '../pages/ContactPage';
+import { TestDataGenerator } from '../utils/TestDataGenerator';
+
+export type TestFixtures = {
+ homePage: HomePage;
+ contactPage: ContactPage;
+ testDataGenerator: typeof TestDataGenerator;
+};
+
+export const test = base.extend({
+ homePage: async ({ page }, use) => {
+ const homePage = new HomePage(page);
+ await use(homePage);
+ },
+
+ contactPage: async ({ page }, use) => {
+ const contactPage = new ContactPage(page);
+ await use(contactPage);
+ },
+
+ testDataGenerator: async ({}, use) => {
+ await use(TestDataGenerator);
+ },
+});
+
+export const expect = test.expect;
diff --git a/e2e/src/pages/BasePage.ts b/e2e/src/pages/BasePage.ts
new file mode 100644
index 0000000..a85a20b
--- /dev/null
+++ b/e2e/src/pages/BasePage.ts
@@ -0,0 +1,151 @@
+import { Page, Locator, expect } from '@playwright/test';
+
+export class BasePage {
+ readonly page: Page;
+
+ constructor(page: Page) {
+ this.page = page;
+ }
+
+ async navigate(url: string): Promise {
+ await this.page.goto(url);
+ }
+
+ async waitForLoadState(state: 'load' | 'domcontentloaded' | 'networkidle' = 'load'): Promise {
+ await this.page.waitForLoadState(state);
+ }
+
+ async click(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ await element.click();
+ }
+
+ async fill(locator: Locator | string, value: string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ await element.fill(value);
+ }
+
+ async getText(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ return await element.textContent() || '';
+ }
+
+ async isVisible(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ return await element.isVisible();
+ }
+
+ async waitForElement(locator: Locator | string, timeout: number = 5000): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ await element.waitFor({ state: 'visible', timeout });
+ }
+
+ async scrollToElement(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ await element.scrollIntoViewIfNeeded();
+ }
+
+ async takeScreenshot(filename: string): Promise {
+ await this.page.screenshot({ path: `test-results/screenshots/${filename}` });
+ }
+
+ async hover(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ await element.hover();
+ }
+
+ async selectOption(locator: Locator | string, value: string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ await element.selectOption(value);
+ }
+
+ async check(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ await element.check();
+ }
+
+ async uncheck(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ await element.uncheck();
+ }
+
+ async waitForURL(url: string | RegExp, timeout: number = 5000): Promise {
+ await this.page.waitForURL(url, { timeout });
+ }
+
+ async getCurrentURL(): Promise {
+ return this.page.url();
+ }
+
+ async getTitle(): Promise {
+ return await this.page.title();
+ }
+
+ async waitForSelector(locator: Locator | string, options?: { state?: 'attached' | 'detached' | 'visible' | 'hidden', timeout?: number }): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ await element.waitFor(options);
+ }
+
+ async getAttribute(locator: Locator | string, attribute: string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ return await element.getAttribute(attribute);
+ }
+
+ async pressKey(key: string): Promise {
+ await this.page.keyboard.press(key);
+ }
+
+ async type(locator: Locator | string, text: string, options?: { delay?: number }): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ await element.type(text, options);
+ }
+
+ async waitForNavigation(options?: { url?: string | RegExp, timeout?: number }): Promise {
+ await this.page.waitForNavigation(options);
+ }
+
+ async reload(): Promise {
+ await this.page.reload();
+ }
+
+ async goBack(): Promise {
+ await this.page.goBack();
+ }
+
+ async goForward(): Promise {
+ await this.page.goForward();
+ }
+
+ async evaluate(pageFunction: () => T): Promise {
+ return await this.page.evaluate(pageFunction);
+ }
+
+ async waitForTimeout(timeout: number): Promise {
+ await this.page.waitForTimeout(timeout);
+ }
+
+ async count(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ return await element.count();
+ }
+
+ async allTextContents(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ return await element.allTextContents();
+ }
+
+ async isDisabled(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ return await element.isDisabled();
+ }
+
+ async isEnabled(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ return await element.isEnabled();
+ }
+
+ async isChecked(locator: Locator | string): Promise {
+ const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
+ return await element.isChecked();
+ }
+}
diff --git a/e2e/src/pages/ContactPage.ts b/e2e/src/pages/ContactPage.ts
new file mode 100644
index 0000000..6f2be6d
--- /dev/null
+++ b/e2e/src/pages/ContactPage.ts
@@ -0,0 +1,307 @@
+import { Page, Locator, expect } from '@playwright/test';
+import { BasePage } from './BasePage';
+import { ContactFormData } from '../types';
+
+export class ContactPage extends BasePage {
+ readonly url: string;
+
+ readonly pageHeader: Locator;
+ readonly contactForm: Locator;
+ readonly nameInput: Locator;
+ readonly phoneInput: Locator;
+ readonly emailInput: Locator;
+ readonly subjectInput: Locator;
+ readonly messageInput: Locator;
+ readonly submitButton: Locator;
+
+ readonly contactInfoCard: Locator;
+ readonly workHoursCard: Locator;
+
+ readonly successMessage: Locator;
+
+ readonly addressInfo: Locator;
+ readonly phoneInfo: Locator;
+ readonly emailInfo: Locator;
+
+ constructor(page: Page) {
+ super(page);
+ this.url = '/contact';
+
+ this.pageHeader = page.locator('h1:has-text("与我们取得联系")');
+ this.contactForm = page.locator('form');
+ this.nameInput = page.locator('input[name="name"]');
+ this.phoneInput = page.locator('input[name="phone"]');
+ this.emailInput = page.locator('input[name="email"]');
+ this.subjectInput = page.locator('input[name="subject"]');
+ this.messageInput = page.locator('textarea[name="message"]');
+ this.submitButton = page.locator('button[type="submit"]');
+
+ this.contactInfoCard = page.locator('[data-slot="card"]').filter({ hasText: '联系方式' }).first();
+ this.workHoursCard = page.locator('[data-slot="card"]').filter({ hasText: '工作时间' }).first();
+
+ this.successMessage = page.locator('text=消息已发送');
+
+ this.addressInfo = this.contactInfoCard.locator('text=公司地址');
+ this.phoneInfo = this.contactInfoCard.locator('text=联系电话');
+ this.emailInfo = this.contactInfoCard.locator('text=电子邮箱');
+ }
+
+ async goto(): Promise {
+ await this.navigate(this.url);
+ await this.waitForLoadState('networkidle');
+ }
+
+ async isLoaded(): Promise {
+ try {
+ await this.pageHeader.waitFor({ state: 'visible', timeout: 5000 });
+ await this.contactForm.waitFor({ state: 'visible', timeout: 5000 });
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ async waitForPageLoad(): Promise {
+ await this.waitForLoadState('networkidle');
+ await this.pageHeader.waitFor({ state: 'visible' });
+ await this.contactForm.waitFor({ state: 'visible' });
+ }
+
+ async fillContactForm(data: ContactFormData): Promise {
+ if (data.name) {
+ await this.nameInput.fill(data.name);
+ }
+ if (data.phone) {
+ await this.phoneInput.fill(data.phone);
+ }
+ if (data.email) {
+ await this.emailInput.fill(data.email);
+ }
+ if (data.subject) {
+ await this.subjectInput.fill(data.subject);
+ }
+ if (data.message) {
+ await this.messageInput.fill(data.message);
+ }
+ }
+
+ async submitForm(): Promise {
+ await this.submitButton.click();
+ }
+
+ async fillAndSubmitForm(data: ContactFormData): Promise {
+ await this.fillContactForm(data);
+ await this.submitForm();
+ }
+
+ async isSuccessMessageVisible(): Promise {
+ return await this.successMessage.isVisible();
+ }
+
+ async getSuccessMessageText(): Promise {
+ return await this.successMessage.textContent() || '';
+ }
+
+ async isFormVisible(): Promise {
+ return await this.contactForm.isVisible();
+ }
+
+ async isSubmitButtonEnabled(): Promise {
+ return await this.submitButton.isEnabled();
+ }
+
+ async getSubmitButtonText(): Promise {
+ return await this.submitButton.textContent() || '';
+ }
+
+ async isSubmitButtonLoading(): Promise {
+ const text = await this.getSubmitButtonText();
+ return text.includes('发送中');
+ }
+
+ async getNameInputValue(): Promise {
+ return await this.nameInput.inputValue();
+ }
+
+ async getPhoneInputValue(): Promise {
+ return await this.phoneInput.inputValue();
+ }
+
+ async getEmailInputValue(): Promise {
+ return await this.emailInput.inputValue();
+ }
+
+ async getSubjectInputValue(): Promise {
+ return await this.subjectInput.inputValue();
+ }
+
+ async getMessageInputValue(): Promise {
+ return await this.messageInput.inputValue();
+ }
+
+ async clearForm(): Promise {
+ await this.nameInput.fill('');
+ await this.phoneInput.fill('');
+ await this.emailInput.fill('');
+ await this.subjectInput.fill('');
+ await this.messageInput.fill('');
+ }
+
+ async isContactInfoCardVisible(): Promise {
+ return await this.contactInfoCard.isVisible();
+ }
+
+ async isWorkHoursCardVisible(): Promise {
+ return await this.workHoursCard.isVisible();
+ }
+
+ async getContactInfoText(): Promise {
+ return await this.contactInfoCard.textContent() || '';
+ }
+
+ async getWorkHoursText(): Promise {
+ return await this.workHoursCard.textContent() || '';
+ }
+
+ async getAddress(): Promise {
+ const addressElement = this.contactInfoCard.locator('div').nth(3).locator('div').nth(1);
+ return await addressElement.textContent() || '';
+ }
+
+ async getPhone(): Promise {
+ const phoneElement = this.contactInfoCard.locator('div').nth(6).locator('div').nth(1);
+ return await phoneElement.textContent() || '';
+ }
+
+ async getEmail(): Promise {
+ const emailElement = this.contactInfoCard.locator('div').nth(9).locator('div').nth(1);
+ return await emailElement.textContent() || '';
+ }
+
+ async getPageTitle(): Promise {
+ return await this.pageHeader.textContent() || '';
+ }
+
+ async getPageDescription(): Promise {
+ const description = this.pageHeader.locator('..').locator('p');
+ return await description.textContent() || '';
+ }
+
+ async getBadgeText(): Promise {
+ const badge = this.page.locator('[data-slot="badge"]').first();
+ return await badge.textContent() || '';
+ }
+
+ async isRequiredFieldVisible(fieldName: string): Promise {
+ const label = this.page.locator(`label[for="${fieldName}"]`);
+ return await label.isVisible();
+ }
+
+ async isFieldRequired(fieldName: string): Promise {
+ const label = this.page.locator(`label[for="${fieldName}"]`);
+ const text = await label.textContent();
+ return text?.includes('*') || false;
+ }
+
+ async getFieldPlaceholder(fieldName: string): Promise {
+ const input = this.page.locator(`[name="${fieldName}"]`);
+ return await input.getAttribute('placeholder') || '';
+ }
+
+ async scrollToForm(): Promise {
+ await this.contactForm.scrollIntoViewIfNeeded();
+ await this.page.waitForTimeout(500);
+ }
+
+ async takeScreenshotOfForm(filename: string): Promise {
+ await this.contactForm.screenshot({ path: `test-results/screenshots/${filename}` });
+ }
+
+ async takeScreenshotOfSuccessMessage(filename: string): Promise {
+ await this.successMessage.screenshot({ path: `test-results/screenshots/${filename}` });
+ }
+
+ async waitForFormSubmission(): Promise {
+ await this.page.waitForTimeout(2000);
+ }
+
+ async isFormSubmitted(): Promise {
+ return await this.isSuccessMessageVisible();
+ }
+
+ async getFormValidationErrors(): Promise {
+ const errors: string[] = [];
+ const requiredInputs = this.contactForm.locator('input[required], textarea[required]');
+ const count = await requiredInputs.count();
+
+ for (let i = 0; i < count; i++) {
+ const input = requiredInputs.nth(i);
+ const isValid = await input.evaluate(el => (el as HTMLInputElement).checkValidity());
+ if (!isValid) {
+ const name = await input.getAttribute('name');
+ errors.push(`${name} is invalid`);
+ }
+ }
+
+ return errors;
+ }
+
+ async isEmailValid(): Promise {
+ return await this.emailInput.evaluate(el => (el as HTMLInputElement).checkValidity());
+ }
+
+ async isPhoneValid(): Promise {
+ return await this.phoneInput.evaluate(el => (el as HTMLInputElement).checkValidity());
+ }
+
+ async focusOnField(fieldName: string): Promise {
+ const input = this.page.locator(`[name="${fieldName}"]`);
+ await input.focus();
+ }
+
+ async blurField(fieldName: string): Promise {
+ const input = this.page.locator(`[name="${fieldName}"]`);
+ await input.blur();
+ }
+
+ async typeInField(fieldName: string, text: string, options?: { delay?: number }): Promise {
+ const input = this.page.locator(`[name="${fieldName}"]`);
+ await input.type(text, options);
+ }
+
+ async clearField(fieldName: string): Promise {
+ const input = this.page.locator(`[name="${fieldName}"]`);
+ await input.fill('');
+ }
+
+ async isFieldVisible(fieldName: string): Promise {
+ const input = this.page.locator(`[name="${fieldName}"]`);
+ return await input.isVisible();
+ }
+
+ async isFieldEnabled(fieldName: string): Promise {
+ const input = this.page.locator(`[name="${fieldName}"]`);
+ return await input.isEnabled();
+ }
+
+ async getFieldAttribute(fieldName: string, attribute: string): Promise {
+ const input = this.page.locator(`[name="${fieldName}"]`);
+ return await input.getAttribute(attribute);
+ }
+
+ async getWorkHours(): Promise<{ day: string; hours: string }[]> {
+ const workHours: { day: string; hours: string }[] = [];
+ const rows = this.workHoursCard.locator('.space-y-2 > div');
+ const count = await rows.count();
+
+ for (let i = 0; i < count; i++) {
+ const row = rows.nth(i);
+ const day = await row.locator('span').first().textContent();
+ const hours = await row.locator('span').nth(1).textContent();
+ if (day && hours) {
+ workHours.push({ day: day.trim(), hours: hours.trim() });
+ }
+ }
+ return workHours;
+ }
+}
diff --git a/e2e/src/pages/HomePage.ts b/e2e/src/pages/HomePage.ts
new file mode 100644
index 0000000..b2bca4e
--- /dev/null
+++ b/e2e/src/pages/HomePage.ts
@@ -0,0 +1,259 @@
+import { Page, Locator, expect } from '@playwright/test';
+import { BasePage } from './BasePage';
+
+export class HomePage extends BasePage {
+ readonly url: string;
+
+ readonly header: Locator;
+ readonly logo: Locator;
+ readonly navigation: Locator;
+ readonly heroSection: Locator;
+ readonly servicesSection: Locator;
+ readonly productsSection: Locator;
+ readonly casesSection: Locator;
+ readonly aboutSection: Locator;
+ readonly newsSection: Locator;
+ readonly contactSection: Locator;
+ readonly footer: Locator;
+
+ readonly mobileMenuButton: Locator;
+ readonly mobileMenu: Locator;
+
+ constructor(page: Page) {
+ super(page);
+ this.url = '/';
+
+ this.header = page.locator('header');
+ this.logo = page.locator('header img[alt*="四川睿新致远"]');
+ this.navigation = page.locator('nav[role="navigation"]');
+ this.heroSection = page.locator('#home');
+ this.servicesSection = page.locator('#services');
+ this.productsSection = page.locator('#products');
+ this.casesSection = page.locator('#cases');
+ this.aboutSection = page.locator('#about');
+ this.newsSection = page.locator('#news');
+ this.contactSection = page.locator('#contact');
+ this.footer = page.locator('footer');
+
+ this.mobileMenuButton = page.locator('button[aria-label="打开菜单"]');
+ this.mobileMenu = page.locator('#mobile-menu-panel');
+ }
+
+ async goto(): Promise {
+ await this.navigate(this.url);
+ await this.waitForLoadState('networkidle');
+ }
+
+ async isLoaded(): Promise {
+ try {
+ await this.header.waitFor({ state: 'visible', timeout: 5000 });
+ await this.heroSection.waitFor({ state: 'visible', timeout: 5000 });
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ async waitForPageLoad(): Promise {
+ await this.waitForLoadState('networkidle');
+ await this.header.waitFor({ state: 'visible' });
+ await this.heroSection.waitFor({ state: 'visible' });
+ }
+
+ async getNavigationItems(): Promise {
+ return await this.navigation.locator('a').all();
+ }
+
+ async clickNavigationItem(label: string): Promise {
+ await this.navigation.locator(`a:has-text("${label}")`).click();
+ }
+
+ async openMobileMenu(): Promise {
+ if (!(await this.mobileMenu.isVisible())) {
+ await this.mobileMenuButton.click();
+ await this.mobileMenu.waitFor({ state: 'visible' });
+ }
+ }
+
+ async closeMobileMenu(): Promise {
+ if (await this.mobileMenu.isVisible()) {
+ await this.mobileMenuButton.click();
+ await this.mobileMenu.waitFor({ state: 'hidden' });
+ }
+ }
+
+ async scrollToSection(sectionId: string): Promise {
+ const section = this.page.locator(`#${sectionId}`);
+ await section.scrollIntoViewIfNeeded();
+ await this.page.waitForTimeout(500);
+ }
+
+ async isSectionVisible(sectionId: string): Promise {
+ const section = this.page.locator(`#${sectionId}`);
+ return await section.isVisible();
+ }
+
+ async getSectionText(sectionId: string): Promise {
+ const section = this.page.locator(`#${sectionId}`);
+ return await section.textContent() || '';
+ }
+
+ async clickContactButton(): Promise {
+ await this.page.locator('a:has-text("立即咨询")').first().click();
+ }
+
+ async isLogoVisible(): Promise {
+ return await this.logo.isVisible();
+ }
+
+ async getLogoAltText(): Promise {
+ return await this.logo.getAttribute('alt');
+ }
+
+ async isFooterVisible(): Promise {
+ return await this.footer.isVisible();
+ }
+
+ async getFooterText(): Promise {
+ return await this.footer.textContent() || '';
+ }
+
+ async waitForHeroSection(): Promise {
+ await this.heroSection.waitFor({ state: 'visible' });
+ }
+
+ async waitForServicesSection(): Promise {
+ await this.scrollToSection('services');
+ await this.servicesSection.waitFor({ state: 'visible' });
+ }
+
+ async waitForProductsSection(): Promise {
+ await this.scrollToSection('products');
+ await this.productsSection.waitFor({ state: 'visible' });
+ }
+
+ async waitForCasesSection(): Promise {
+ await this.scrollToSection('cases');
+ await this.casesSection.waitFor({ state: 'visible' });
+ }
+
+ async waitForAboutSection(): Promise {
+ await this.scrollToSection('about');
+ await this.aboutSection.waitFor({ state: 'visible' });
+ }
+
+ async waitForNewsSection(): Promise {
+ await this.scrollToSection('news');
+ await this.newsSection.waitFor({ state: 'visible' });
+ }
+
+ async waitForContactSection(): Promise {
+ await this.scrollToSection('contact');
+ await this.contactSection.waitFor({ state: 'visible' });
+ }
+
+ async scrollToBottom(): Promise {
+ await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
+ await this.page.waitForTimeout(500);
+ }
+
+ async scrollToTop(): Promise {
+ await this.page.evaluate(() => window.scrollTo(0, 0));
+ await this.page.waitForTimeout(500);
+ }
+
+ async getActiveNavigationItem(): Promise {
+ const activeItem = this.navigation.locator('a[aria-current="page"]');
+ if (await activeItem.count() > 0) {
+ return await activeItem.textContent();
+ }
+ return null;
+ }
+
+ async isNavigationItemActive(label: string): Promise {
+ const item = this.navigation.locator(`a:has-text("${label}")`);
+ const ariaCurrent = await item.getAttribute('aria-current');
+ return ariaCurrent === 'page';
+ }
+
+ async getAllSectionIds(): Promise {
+ return await this.page.evaluate(() => {
+ const sections = document.querySelectorAll('section[id]');
+ return Array.from(sections).map(section => section.id);
+ });
+ }
+
+ async takeScreenshotOfSection(sectionId: string, filename: string): Promise {
+ const section = this.page.locator(`#${sectionId}`);
+ await section.screenshot({ path: `test-results/screenshots/${filename}` });
+ }
+
+ async getHeroSectionTitle(): Promise {
+ const title = this.heroSection.locator('h1, h2').first();
+ return await title.textContent() || '';
+ }
+
+ async getServicesSectionTitle(): Promise {
+ const title = this.servicesSection.locator('h2').first();
+ return await title.textContent() || '';
+ }
+
+ async getProductsSectionTitle(): Promise {
+ const title = this.productsSection.locator('h2').first();
+ return await title.textContent() || '';
+ }
+
+ async getCasesSectionTitle(): Promise {
+ const title = this.casesSection.locator('h2').first();
+ return await title.textContent() || '';
+ }
+
+ async getAboutSectionTitle(): Promise {
+ const title = this.aboutSection.locator('h2').first();
+ return await title.textContent() || '';
+ }
+
+ async getNewsSectionTitle(): Promise {
+ const title = this.newsSection.locator('h2').first();
+ return await title.textContent() || '';
+ }
+
+ async getContactSectionTitle(): Promise {
+ const title = this.contactSection.locator('h2').first();
+ return await title.textContent() || '';
+ }
+
+ async isHeaderSticky(): Promise {
+ const isSticky = await this.header.evaluate(el => {
+ return window.getComputedStyle(el).position === 'fixed';
+ });
+ return isSticky;
+ }
+
+ async getHeaderBackgroundColor(): Promise {
+ return await this.header.evaluate(el => {
+ return window.getComputedStyle(el).backgroundColor;
+ });
+ }
+
+ async isHeaderScrolled(): Promise {
+ const hasShadow = await this.header.evaluate(el => {
+ return window.getComputedStyle(el).boxShadow !== 'none';
+ });
+ return hasShadow;
+ }
+
+ async getNavigationItemCount(): Promise {
+ return await this.navigation.locator('a').count();
+ }
+
+ async getAllNavigationLabels(): Promise {
+ const items = await this.getNavigationItems();
+ const labels: string[] = [];
+ for (const item of items) {
+ const text = await item.textContent();
+ if (text) labels.push(text);
+ }
+ return labels;
+ }
+}
diff --git a/e2e/src/tests/debug/contact-all-elements.spec.ts b/e2e/src/tests/debug/contact-all-elements.spec.ts
new file mode 100644
index 0000000..03438e4
--- /dev/null
+++ b/e2e/src/tests/debug/contact-all-elements.spec.ts
@@ -0,0 +1,56 @@
+import { test, expect } from '@playwright/test';
+
+test('检查联系页面所有元素', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const badges = await page.locator('[class*="badge"]').all();
+ console.log('找到的badge数量:', badges.length);
+
+ for (let i = 0; i < badges.length; i++) {
+ const badge = badges[i];
+ const className = await badge.evaluate(el => el.className);
+ const text = await badge.textContent();
+ console.log(`Badge ${i}: ${className} - ${text}`);
+ }
+
+ const contactCard = page.locator('[class*="card"]').filter({ hasText: '联系方式' }).first();
+ const contactCardText = await contactCard.textContent();
+ console.log('联系卡片文本:', contactCardText?.substring(0, 100));
+
+ const workHoursCard = page.locator('[class*="card"]').filter({ hasText: '工作时间' }).first();
+ const workHoursCardText = await workHoursCard.textContent();
+ console.log('工作时间卡片文本:', workHoursCardText?.substring(0, 100));
+
+ const addressH3 = contactCard.locator('h3:has-text("公司地址")');
+ const addressParent = addressH3.locator('..');
+ const addressGrandParent = addressParent.locator('..');
+ const addressP = addressGrandParent.locator('p');
+ const addressText = await addressP.textContent();
+ console.log('地址文本:', addressText);
+
+ const phoneH3 = contactCard.locator('h3:has-text("联系电话")');
+ const phoneParent = phoneH3.locator('..');
+ const phoneGrandParent = phoneParent.locator('..');
+ const phoneP = phoneGrandParent.locator('p');
+ const phoneText = await phoneP.textContent();
+ console.log('电话文本:', phoneText);
+
+ const emailH3 = contactCard.locator('h3:has-text("电子邮箱")');
+ const emailParent = emailH3.locator('..');
+ const emailGrandParent = emailParent.locator('..');
+ const emailP = emailGrandParent.locator('p');
+ const emailText = await emailP.textContent();
+ console.log('邮箱文本:', emailText);
+
+ const workHoursRows = workHoursCard.locator('.space-y-2 > div');
+ const workHoursCount = await workHoursRows.count();
+ console.log('工作时间行数:', workHoursCount);
+
+ for (let i = 0; i < workHoursCount; i++) {
+ const row = workHoursRows.nth(i);
+ const day = await row.locator('span').first().textContent();
+ const hours = await row.locator('span').nth(1).textContent();
+ console.log(`工作时间 ${i}: ${day} - ${hours}`);
+ }
+});
diff --git a/e2e/src/tests/debug/contact-card-structure.spec.ts b/e2e/src/tests/debug/contact-card-structure.spec.ts
new file mode 100644
index 0000000..b1d7833
--- /dev/null
+++ b/e2e/src/tests/debug/contact-card-structure.spec.ts
@@ -0,0 +1,21 @@
+import { test, expect } from '@playwright/test';
+
+test('检查联系卡片详细结构', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const contactCard = page.locator('[data-slot="card"]').filter({ hasText: '联系方式' }).first();
+
+ const contactCardChildren = await contactCard.locator('div').all();
+ console.log('联系卡片子元素数量:', contactCardChildren.length);
+
+ for (let i = 0; i < contactCardChildren.length; i++) {
+ const child = contactCardChildren[i];
+ const className = await child.evaluate(el => el.className);
+ const text = await child.textContent();
+ console.log(`子元素 ${i}: ${className.substring(0, 80)} - ${text?.substring(0, 50)}`);
+ }
+
+ const allDivs = await contactCard.locator('div').all();
+ console.log('所有div数量:', allDivs.length);
+});
diff --git a/e2e/src/tests/debug/contact-card-titles.spec.ts b/e2e/src/tests/debug/contact-card-titles.spec.ts
new file mode 100644
index 0000000..dcd9445
--- /dev/null
+++ b/e2e/src/tests/debug/contact-card-titles.spec.ts
@@ -0,0 +1,25 @@
+import { test, expect } from '@playwright/test';
+
+test('检查联系页面Card标题', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const cardTitles = await page.locator('CardTitle').all();
+ console.log('找到的CardTitle数量:', cardTitles.length);
+
+ for (let i = 0; i < cardTitles.length; i++) {
+ const title = cardTitles[i];
+ const text = await title.textContent();
+ console.log(`CardTitle ${i}: ${text}`);
+ }
+
+ const allCards = await page.locator('[class*="card"]').all();
+ console.log('找到的所有card元素数量:', allCards.length);
+
+ for (let i = 0; i < allCards.length; i++) {
+ const card = allCards[i];
+ const className = await card.evaluate(el => el.className);
+ const text = await card.textContent();
+ console.log(`Card ${i}: ${className.substring(0, 50)} - ${text?.substring(0, 50)}`);
+ }
+});
diff --git a/e2e/src/tests/debug/contact-cards.spec.ts b/e2e/src/tests/debug/contact-cards.spec.ts
new file mode 100644
index 0000000..367af81
--- /dev/null
+++ b/e2e/src/tests/debug/contact-cards.spec.ts
@@ -0,0 +1,23 @@
+import { test, expect } from '@playwright/test';
+
+test('检查联系卡片详细信息', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const contactCard = page.locator('.card').filter({ hasText: '联系方式' });
+ const contactText = await contactCard.textContent();
+ console.log('联系方式卡片内容:', contactText);
+
+ const workHoursCard = page.locator('.card').filter({ hasText: '工作时间' });
+ const workHoursText = await workHoursCard.textContent();
+ console.log('工作时间卡片内容:', workHoursText);
+
+ const allCards = await page.locator('.card').all();
+ console.log('所有卡片数量:', allCards.length);
+
+ for (let i = 0; i < allCards.length; i++) {
+ const card = allCards[i];
+ const text = await card.textContent();
+ console.log(`卡片 ${i}:`, text?.substring(0, 100));
+ }
+});
diff --git a/e2e/src/tests/debug/contact-details.spec.ts b/e2e/src/tests/debug/contact-details.spec.ts
new file mode 100644
index 0000000..a04f0fa
--- /dev/null
+++ b/e2e/src/tests/debug/contact-details.spec.ts
@@ -0,0 +1,41 @@
+import { test, expect } from '@playwright/test';
+
+test('检查联系页面详细元素', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const pageHeader = page.locator('h1:has-text("与我们取得联系")');
+ const pageHeaderParent = pageHeader.locator('..');
+ const pageHeaderGrandParent = pageHeaderParent.locator('..');
+
+ console.log('Page Header parent:', await pageHeaderParent.evaluate(el => el.className));
+ console.log('Page Header grand parent:', await pageHeaderGrandParent.evaluate(el => el.className));
+
+ const badges = await pageHeaderGrandParent.locator('.badge').all();
+ console.log('找到的badge数量:', badges.length);
+
+ for (let i = 0; i < badges.length; i++) {
+ const badge = badges[i];
+ const text = await badge.textContent();
+ console.log(`Badge ${i}: ${text}`);
+ }
+
+ const contactCard = page.locator('h3:has-text("联系方式")');
+ const contactCardParent = contactCard.locator('..');
+ const contactCardGrandParent = contactCardParent.locator('..');
+ const contactCardGreatGrandParent = contactCardGrandParent.locator('..');
+
+ console.log('Contact card great grand parent:', await contactCardGreatGrandParent.evaluate(el => el.className));
+
+ const addressElement = contactCard.locator('text=公司地址').locator('..').locator('p');
+ const addressText = await addressElement.textContent();
+ console.log('地址:', addressText);
+
+ const phoneElement = contactCard.locator('text=联系电话').locator('..').locator('p');
+ const phoneText = await phoneElement.textContent();
+ console.log('电话:', phoneText);
+
+ const emailElement = contactCard.locator('text=电子邮箱').locator('..').locator('p');
+ const emailText = await emailElement.textContent();
+ console.log('邮箱:', emailText);
+});
diff --git a/e2e/src/tests/debug/contact-dom.spec.ts b/e2e/src/tests/debug/contact-dom.spec.ts
new file mode 100644
index 0000000..b2d0c18
--- /dev/null
+++ b/e2e/src/tests/debug/contact-dom.spec.ts
@@ -0,0 +1,21 @@
+import { test, expect } from '@playwright/test';
+
+test('检查联系页面完整DOM', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const contactCard = page.locator('[class*="card"]').filter({ hasText: '联系方式' }).first();
+
+ const contactCardHTML = await contactCard.innerHTML();
+ console.log('联系卡片HTML:', contactCardHTML.substring(0, 500));
+
+ const contactCardChildren = await contactCard.locator('div').all();
+ console.log('联系卡片子元素数量:', contactCardChildren.length);
+
+ for (let i = 0; i < contactCardChildren.length; i++) {
+ const child = contactCardChildren[i];
+ const className = await child.evaluate(el => el.className);
+ const text = await child.textContent();
+ console.log(`子元素 ${i}: ${className.substring(0, 50)} - ${text?.substring(0, 50)}`);
+ }
+});
diff --git a/e2e/src/tests/debug/contact-hierarchy.spec.ts b/e2e/src/tests/debug/contact-hierarchy.spec.ts
new file mode 100644
index 0000000..ffabd87
--- /dev/null
+++ b/e2e/src/tests/debug/contact-hierarchy.spec.ts
@@ -0,0 +1,30 @@
+import { test, expect } from '@playwright/test';
+
+test('检查联系页面完整结构', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const allH3 = await page.locator('h3').all();
+ console.log('找到的h3元素数量:', allH3.length);
+
+ for (let i = 0; i < allH3.length; i++) {
+ const h3 = allH3[i];
+ const text = await h3.textContent();
+ console.log(`H3 ${i}: ${text}`);
+
+ const parent = h3.locator('..');
+ const grandParent = parent.locator('..');
+ const greatGrandParent = grandParent.locator('..');
+
+ try {
+ const parentClass = await parent.evaluate(el => el.className);
+ const grandParentClass = await grandParent.evaluate(el => el.className);
+ const greatGrandParentClass = await greatGrandParent.evaluate(el => el.className);
+ console.log(` Parent: ${parentClass}`);
+ console.log(` Grand Parent: ${grandParentClass}`);
+ console.log(` Great Grand Parent: ${greatGrandParentClass}`);
+ } catch (e) {
+ console.log(` Error getting parent info: ${e}`);
+ }
+ }
+});
diff --git a/e2e/src/tests/debug/contact-page.spec.ts b/e2e/src/tests/debug/contact-page.spec.ts
new file mode 100644
index 0000000..76f8e33
--- /dev/null
+++ b/e2e/src/tests/debug/contact-page.spec.ts
@@ -0,0 +1,34 @@
+import { test, expect } from '@playwright/test';
+
+test('检查联系页面元素', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const pageHeaders = await page.locator('.page-header').all();
+ console.log('找到的page-header数量:', pageHeaders.length);
+
+ const forms = await page.locator('form').all();
+ console.log('找到的form数量:', forms.length);
+
+ const cards = await page.locator('.card').all();
+ console.log('找到的card数量:', cards.length);
+
+ for (let i = 0; i < cards.length; i++) {
+ const card = cards[i];
+ const text = await card.textContent();
+ console.log(`Card ${i}: ${text?.substring(0, 50)}`);
+ }
+
+ const inputs = await page.locator('input').all();
+ console.log('找到的input数量:', inputs.length);
+
+ for (let i = 0; i < inputs.length; i++) {
+ const input = inputs[i];
+ const name = await input.getAttribute('name');
+ const type = await input.getAttribute('type');
+ console.log(`Input ${i}: name="${name}", type="${type}"`);
+ }
+
+ const textareas = await page.locator('textarea').all();
+ console.log('找到的textarea数量:', textareas.length);
+});
diff --git a/e2e/src/tests/debug/contact-structure.spec.ts b/e2e/src/tests/debug/contact-structure.spec.ts
new file mode 100644
index 0000000..165602f
--- /dev/null
+++ b/e2e/src/tests/debug/contact-structure.spec.ts
@@ -0,0 +1,28 @@
+import { test, expect } from '@playwright/test';
+
+test('检查联系页面结构', async ({ page }) => {
+ await page.goto('/contact');
+ await page.waitForLoadState('networkidle');
+
+ const allElements = await page.evaluate(() => {
+ const result: any = {};
+
+ const h1s = document.querySelectorAll('h1');
+ result.h1Count = h1s.length;
+ result.h1Text = Array.from(h1s).map(h => h.textContent?.substring(0, 50));
+
+ const cards = document.querySelectorAll('[class*="card"]');
+ result.cardCount = cards.length;
+ result.cardText = Array.from(cards).map(c => c.textContent?.substring(0, 50));
+
+ const pageHeaders = document.querySelectorAll('[class*="page-header"]');
+ result.pageHeaderCount = pageHeaders.length;
+
+ const contactInfoCards = document.querySelectorAll('[class*="contact"]');
+ result.contactInfoCount = contactInfoCards.length;
+
+ return result;
+ });
+
+ console.log('联系页面结构:', JSON.stringify(allElements, null, 2));
+});
diff --git a/e2e/src/tests/debug/page-load.spec.ts b/e2e/src/tests/debug/page-load.spec.ts
new file mode 100644
index 0000000..ff9cd5f
--- /dev/null
+++ b/e2e/src/tests/debug/page-load.spec.ts
@@ -0,0 +1,37 @@
+import { test, expect } from '@playwright/test';
+
+test('测试页面加载', async ({ page }) => {
+ await page.goto('/');
+ await page.waitForLoadState('networkidle');
+
+ const title = await page.title();
+ console.log('页面标题:', title);
+
+ const body = await page.locator('body').textContent();
+ console.log('页面内容长度:', body?.length);
+
+ const headers = await page.locator('header').all();
+ console.log('找到的header元素数量:', headers.length);
+
+ const navs = await page.locator('nav').all();
+ console.log('找到的nav元素数量:', navs.length);
+
+ const sections = await page.locator('section').all();
+ console.log('找到的section元素数量:', sections.length);
+
+ const allElements = await page.evaluate(() => {
+ const headers = document.querySelectorAll('header');
+ const navs = document.querySelectorAll('nav');
+ const sections = document.querySelectorAll('section');
+ return {
+ headers: headers.length,
+ navs: navs.length,
+ sections: sections.length,
+ bodyText: document.body.textContent?.substring(0, 200)
+ };
+ });
+
+ console.log('页面元素信息:', allElements);
+
+ expect(title).toBeTruthy();
+});
diff --git a/e2e/src/tests/debug/selectors.spec.ts b/e2e/src/tests/debug/selectors.spec.ts
new file mode 100644
index 0000000..81d117e
--- /dev/null
+++ b/e2e/src/tests/debug/selectors.spec.ts
@@ -0,0 +1,36 @@
+import { test, expect } from '@playwright/test';
+
+test('检查页面元素选择器', async ({ page }) => {
+ await page.goto('/');
+ await page.waitForLoadState('networkidle');
+
+ const logoImages = await page.locator('header img').all();
+ console.log('找到的logo图片数量:', logoImages.length);
+
+ for (let i = 0; i < logoImages.length; i++) {
+ const img = logoImages[i];
+ const alt = await img.getAttribute('alt');
+ const src = await img.getAttribute('src');
+ console.log(`Logo ${i}: alt="${alt}", src="${src}"`);
+ }
+
+ const contactButtons = await page.locator('a:has-text("立即咨询")').all();
+ console.log('找到的立即咨询按钮数量:', contactButtons.length);
+
+ for (let i = 0; i < contactButtons.length; i++) {
+ const btn = contactButtons[i];
+ const href = await btn.getAttribute('href');
+ const text = await btn.textContent();
+ console.log(`立即咨询按钮 ${i}: text="${text}", href="${href}"`);
+ }
+
+ const mobileMenuButtons = await page.locator('button').all();
+ console.log('找到的按钮数量:', mobileMenuButtons.length);
+
+ for (let i = 0; i < mobileMenuButtons.length; i++) {
+ const btn = mobileMenuButtons[i];
+ const ariaLabel = await btn.getAttribute('aria-label');
+ const text = await btn.textContent();
+ console.log(`按钮 ${i}: aria-label="${ariaLabel}", text="${text}"`);
+ }
+});
diff --git a/e2e/src/tests/performance/interaction-performance.spec.ts b/e2e/src/tests/performance/interaction-performance.spec.ts
new file mode 100644
index 0000000..7a0a2a4
--- /dev/null
+++ b/e2e/src/tests/performance/interaction-performance.spec.ts
@@ -0,0 +1,376 @@
+import { test, expect } from '../../fixtures/base.fixture';
+import { PerformanceMonitor } from '../../utils/PerformanceMonitor';
+
+test.describe('交互性能测试 @performance', () => {
+ test('点击导航项应该快速响应', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ const labels = await homePage.getAllNavigationLabels();
+ if (labels.length > 0) {
+ await homePage.clickNavigationItem(labels[0]);
+ }
+ await homePage.page.waitForTimeout(100);
+ const endTime = Date.now();
+
+ const clickDuration = endTime - startTime;
+ console.log('导航项点击响应时间:', clickDuration, 'ms');
+
+ expect(clickDuration).toBeLessThan(500);
+ });
+
+ test('滚动应该流畅', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ for (let i = 0; i < 20; i++) {
+ await homePage.page.evaluate(() => window.scrollBy(0, 100));
+ await homePage.page.waitForTimeout(30);
+ }
+ const endTime = Date.now();
+
+ const scrollDuration = endTime - startTime;
+ console.log('滚动持续时间:', scrollDuration, 'ms');
+
+ expect(scrollDuration).toBeLessThan(1500);
+ });
+
+ test('表单输入应该快速响应', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await contactPage.nameInput.fill('测试用户');
+ await contactPage.emailInput.fill('test@example.com');
+ await contactPage.subjectInput.fill('测试主题');
+ await contactPage.messageInput.fill('这是一条测试消息');
+ const endTime = Date.now();
+
+ const inputDuration = endTime - startTime;
+ console.log('表单输入持续时间:', inputDuration, 'ms');
+
+ expect(inputDuration).toBeLessThan(1000);
+ });
+
+ test('按钮点击应该快速响应', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await homePage.clickContactButton();
+ await homePage.page.waitForTimeout(100);
+ const endTime = Date.now();
+
+ const clickDuration = endTime - startTime;
+ console.log('按钮点击响应时间:', clickDuration, 'ms');
+
+ expect(clickDuration).toBeLessThan(500);
+ });
+
+ test('移动端菜单打开应该快速', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await homePage.openMobileMenu();
+ const endTime = Date.now();
+
+ const menuOpenDuration = endTime - startTime;
+ console.log('移动端菜单打开时间:', menuOpenDuration, 'ms');
+
+ expect(menuOpenDuration).toBeLessThan(500);
+ });
+
+ test('移动端菜单关闭应该快速', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.openMobileMenu();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await homePage.closeMobileMenu();
+ const endTime = Date.now();
+
+ const menuCloseDuration = endTime - startTime;
+ console.log('移动端菜单关闭时间:', menuCloseDuration, 'ms');
+
+ expect(menuCloseDuration).toBeLessThan(500);
+ });
+
+ test('页面跳转应该快速', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await homePage.clickContactButton();
+ await homePage.page.waitForLoadState('networkidle');
+ const endTime = Date.now();
+
+ const navigationDuration = endTime - startTime;
+ console.log('页面跳转持续时间:', navigationDuration, 'ms');
+
+ expect(navigationDuration).toBeLessThan(2000);
+ });
+
+ test('表单提交应该快速', async ({ contactPage, page, testDataGenerator }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const formData = testDataGenerator.generateContactFormData();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await contactPage.fillContactForm(formData);
+ await contactPage.submitForm();
+ await contactPage.waitForFormSubmission();
+ const endTime = Date.now();
+
+ const submissionDuration = endTime - startTime;
+ console.log('表单提交持续时间:', submissionDuration, 'ms');
+
+ expect(submissionDuration).toBeLessThan(3000);
+ });
+
+ test('悬停效果应该流畅', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ const labels = await homePage.getAllNavigationLabels();
+ if (labels.length > 0) {
+ const firstNavItem = homePage.navigation.locator('a').first();
+ await firstNavItem.hover();
+ await homePage.page.waitForTimeout(100);
+ }
+ const endTime = Date.now();
+
+ const hoverDuration = endTime - startTime;
+ console.log('悬停效果持续时间:', hoverDuration, 'ms');
+
+ expect(hoverDuration).toBeLessThan(300);
+ });
+
+ test('快速连续点击应该正常响应', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ const labels = await homePage.getAllNavigationLabels();
+ for (let i = 0; i < Math.min(labels.length, 3); i++) {
+ await homePage.clickNavigationItem(labels[i]);
+ await homePage.page.waitForTimeout(200);
+ }
+ const endTime = Date.now();
+
+ const rapidClickDuration = endTime - startTime;
+ console.log('快速连续点击持续时间:', rapidClickDuration, 'ms');
+
+ expect(rapidClickDuration).toBeLessThan(2000);
+ });
+
+ test('快速滚动应该流畅', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ for (let i = 0; i < 10; i++) {
+ await homePage.page.evaluate(() => window.scrollBy(0, 500));
+ await homePage.page.waitForTimeout(50);
+ }
+ const endTime = Date.now();
+
+ const rapidScrollDuration = endTime - startTime;
+ console.log('快速滚动持续时间:', rapidScrollDuration, 'ms');
+
+ expect(rapidScrollDuration).toBeLessThan(1500);
+ });
+
+ test('表单验证应该快速', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await contactPage.emailInput.fill('invalid-email');
+ await contactPage.page.waitForTimeout(100);
+ const isValid = await contactPage.isEmailValid();
+ const endTime = Date.now();
+
+ const validationDuration = endTime - startTime;
+ console.log('表单验证持续时间:', validationDuration, 'ms');
+
+ expect(validationDuration).toBeLessThan(300);
+ expect(isValid).toBe(false);
+ });
+
+ test('键盘导航应该流畅', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ for (let i = 0; i < 10; i++) {
+ await homePage.page.keyboard.press('Tab');
+ await homePage.page.waitForTimeout(50);
+ }
+ const endTime = Date.now();
+
+ const keyboardNavDuration = endTime - startTime;
+ console.log('键盘导航持续时间:', keyboardNavDuration, 'ms');
+
+ expect(keyboardNavDuration).toBeLessThan(1000);
+ });
+
+ test('页面重新加载应该快速', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await homePage.reload();
+ await homePage.waitForPageLoad();
+ const endTime = Date.now();
+
+ const reloadDuration = endTime - startTime;
+ console.log('页面重新加载持续时间:', reloadDuration, 'ms');
+
+ expect(reloadDuration).toBeLessThan(2000);
+ });
+
+ test('返回上一页应该快速', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.clickContactButton();
+ await homePage.page.waitForLoadState('networkidle');
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await homePage.goBack();
+ await homePage.page.waitForLoadState('networkidle');
+ const endTime = Date.now();
+
+ const backDuration = endTime - startTime;
+ console.log('返回上一页持续时间:', backDuration, 'ms');
+
+ expect(backDuration).toBeLessThan(1500);
+ });
+
+ test('前进到下一页应该快速', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.clickContactButton();
+ await homePage.page.waitForLoadState('networkidle');
+ await homePage.goBack();
+ await homePage.page.waitForLoadState('networkidle');
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await homePage.goForward();
+ await homePage.page.waitForLoadState('networkidle');
+ const endTime = Date.now();
+
+ const forwardDuration = endTime - startTime;
+ console.log('前进到下一页持续时间:', forwardDuration, 'ms');
+
+ expect(forwardDuration).toBeLessThan(1500);
+ });
+
+ test('窗口大小调整应该流畅', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const startTime = Date.now();
+ await homePage.page.setViewportSize({ width: 768, height: 1024 });
+ await homePage.page.waitForTimeout(100);
+ await homePage.page.setViewportSize({ width: 1280, height: 720 });
+ await homePage.page.waitForTimeout(100);
+ const endTime = Date.now();
+
+ const resizeDuration = endTime - startTime;
+ console.log('窗口大小调整持续时间:', resizeDuration, 'ms');
+
+ expect(resizeDuration).toBeLessThan(500);
+ });
+
+ test('所有交互应该在合理时间内完成', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ const interactions: { name: string; duration: number }[] = [];
+
+ const startClick = Date.now();
+ await homePage.clickContactButton();
+ await homePage.page.waitForLoadState('networkidle');
+ interactions.push({ name: '点击按钮', duration: Date.now() - startClick });
+
+ await homePage.goBack();
+ const startScroll = Date.now();
+ await homePage.scrollToSection('services');
+ interactions.push({ name: '滚动到区块', duration: Date.now() - startScroll });
+
+ const startNav = Date.now();
+ const labels = await homePage.getAllNavigationLabels();
+ if (labels.length > 0) {
+ await homePage.clickNavigationItem(labels[0]);
+ }
+ interactions.push({ name: '导航点击', duration: Date.now() - startNav });
+
+ console.log('交互性能汇总:');
+ interactions.forEach(interaction => {
+ console.log(`- ${interaction.name}: ${interaction.duration}ms`);
+ });
+
+ interactions.forEach(interaction => {
+ expect(interaction.duration).toBeLessThan(2000);
+ });
+ });
+});
diff --git a/e2e/src/tests/performance/performance.spec.ts b/e2e/src/tests/performance/performance.spec.ts
new file mode 100644
index 0000000..eadb3e4
--- /dev/null
+++ b/e2e/src/tests/performance/performance.spec.ts
@@ -0,0 +1,271 @@
+import { test, expect } from '../../fixtures/base.fixture';
+import { PerformanceMonitor } from '../../utils/PerformanceMonitor';
+import { PerformanceThresholds } from '../../types';
+
+const performanceThresholds: PerformanceThresholds = {
+ loadTime: 3000,
+ firstContentfulPaint: 1800,
+ largestContentfulPaint: 2500,
+ timeToInteractive: 3500,
+ cumulativeLayoutShift: 0.1,
+ firstInputDelay: 100,
+};
+
+test.describe('性能测试 @performance', () => {
+ test('首页加载时间应该在阈值范围内', async ({ homePage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const metrics = await monitor.collectMetrics();
+ const validation = monitor.validateMetrics(performanceThresholds);
+
+ console.log('首页性能指标:', metrics);
+ console.log('验证结果:', validation);
+
+ expect(validation.passed).toBe(true);
+ expect(metrics.loadTime).toBeLessThan(performanceThresholds.loadTime);
+ });
+
+ test('联系页面加载时间应该在阈值范围内', async ({ contactPage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const metrics = await monitor.collectMetrics();
+ const validation = monitor.validateMetrics(performanceThresholds);
+
+ console.log('联系页面性能指标:', metrics);
+ console.log('验证结果:', validation);
+
+ expect(validation.passed).toBe(true);
+ expect(metrics.loadTime).toBeLessThan(performanceThresholds.loadTime);
+ });
+
+ test('首次内容绘制应该在1.8秒内完成', async ({ homePage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const fcp = await monitor.measureFirstContentfulPaint();
+
+ console.log('首次内容绘制时间:', fcp, 'ms');
+
+ expect(fcp).toBeLessThan(performanceThresholds.firstContentfulPaint);
+ expect(fcp).toBeGreaterThan(0);
+ });
+
+ test('最大内容绘制应该在2.5秒内完成', async ({ homePage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const lcp = await monitor.measureLargestContentfulPaint();
+
+ console.log('最大内容绘制时间:', lcp, 'ms');
+
+ expect(lcp).toBeLessThan(performanceThresholds.largestContentfulPaint);
+ expect(lcp).toBeGreaterThan(0);
+ });
+
+ test('累积布局偏移应该小于0.1', async ({ homePage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.scrollToSection('services');
+ await homePage.scrollToSection('products');
+ await homePage.scrollToSection('cases');
+
+ const cls = await monitor.measureCumulativeLayoutShift();
+
+ console.log('累积布局偏移:', cls);
+
+ expect(cls).toBeLessThan(performanceThresholds.cumulativeLayoutShift);
+ expect(cls).toBeGreaterThanOrEqual(0);
+ });
+
+ test('首次输入延迟应该小于100ms', async ({ homePage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.logo.click();
+
+ const fid = await monitor.measureFirstInputDelay();
+
+ console.log('首次输入延迟:', fid, 'ms');
+
+ expect(fid).toBeLessThan(performanceThresholds.firstInputDelay);
+ expect(fid).toBeGreaterThanOrEqual(0);
+ });
+
+ test('可交互时间应该在3.5秒内完成', async ({ homePage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const tti = await monitor.measureTimeToInteractive();
+
+ console.log('可交互时间:', tti, 'ms');
+
+ expect(tti).toBeLessThan(performanceThresholds.timeToInteractive);
+ expect(tti).toBeGreaterThan(0);
+ });
+
+ test('页面应该有良好的帧率', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const monitor = new PerformanceMonitor(page);
+ const frameRate = await monitor.measureFrameRate();
+
+ console.log('帧率:', frameRate, 'FPS');
+
+ expect(frameRate).toBeGreaterThan(30);
+ });
+
+ test('资源加载应该高效', async ({ homePage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const resources = await monitor.measureResourceTiming();
+ const totalSize = resources.reduce((sum, r) => sum + (r.size || 0), 0);
+ const totalSizeKB = totalSize / 1024;
+
+ console.log('总资源大小:', totalSizeKB.toFixed(2), 'KB');
+ console.log('资源数量:', resources.length);
+
+ expect(totalSizeKB).toBeLessThan(3000);
+ expect(resources.length).toBeGreaterThan(0);
+ });
+
+ test('DOM内容加载应该快速', async ({ homePage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await homePage.goto();
+
+ const dcl = await monitor.measureDomContentLoaded();
+
+ console.log('DOM内容加载时间:', dcl, 'ms');
+
+ expect(dcl).toBeLessThan(2000);
+ expect(dcl).toBeGreaterThan(0);
+ });
+
+ test('应该生成性能报告', async ({ homePage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const report = await monitor.generateReport();
+
+ console.log('性能报告:');
+ console.log(report);
+
+ expect(report).toContain('页面加载时间');
+ expect(report).toContain('首次内容绘制');
+ expect(report).toContain('最大内容绘制');
+ expect(report).toContain('累积布局偏移');
+ expect(report).toContain('资源加载');
+ });
+
+ test('滚动性能应该良好', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const startTime = Date.now();
+
+ for (let i = 0; i < 10; i++) {
+ await homePage.page.evaluate(() => window.scrollBy(0, 200));
+ await homePage.page.waitForTimeout(50);
+ }
+
+ const endTime = Date.now();
+ const scrollDuration = endTime - startTime;
+
+ console.log('滚动持续时间:', scrollDuration, 'ms');
+
+ expect(scrollDuration).toBeLessThan(2000);
+ });
+
+ test('导航性能应该良好', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const startTime = Date.now();
+ await homePage.clickContactButton();
+ await homePage.page.waitForLoadState('networkidle');
+ const endTime = Date.now();
+
+ const navigationDuration = endTime - startTime;
+
+ console.log('导航持续时间:', navigationDuration, 'ms');
+
+ expect(navigationDuration).toBeLessThan(2000);
+ });
+
+ test('表单提交性能应该良好', async ({ contactPage, page, testDataGenerator }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const formData = testDataGenerator.generateContactFormData();
+
+ const startTime = Date.now();
+ await contactPage.fillContactForm(formData);
+ await contactPage.submitForm();
+ await contactPage.waitForFormSubmission();
+ const endTime = Date.now();
+
+ const submissionDuration = endTime - startTime;
+
+ console.log('表单提交持续时间:', submissionDuration, 'ms');
+
+ expect(submissionDuration).toBeLessThan(3000);
+ });
+
+ test('所有核心性能指标应该符合标准', async ({ homePage, page }) => {
+ const monitor = new PerformanceMonitor(page);
+ await monitor.startMonitoring();
+
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const metrics = await monitor.collectMetrics();
+ const validation = monitor.validateMetrics(performanceThresholds);
+
+ console.log('完整性能指标:', metrics);
+ console.log('验证结果:', validation);
+
+ if (!validation.passed) {
+ console.error('性能违规:', validation.violations);
+ }
+
+ expect(metrics.loadTime).toBeLessThan(performanceThresholds.loadTime);
+ expect(metrics.firstContentfulPaint).toBeLessThan(performanceThresholds.firstContentfulPaint);
+ expect(metrics.largestContentfulPaint).toBeLessThan(performanceThresholds.largestContentfulPaint);
+ expect(metrics.timeToInteractive).toBeLessThan(performanceThresholds.timeToInteractive);
+ expect(metrics.cumulativeLayoutShift).toBeLessThan(performanceThresholds.cumulativeLayoutShift);
+ expect(metrics.firstInputDelay).toBeLessThan(performanceThresholds.firstInputDelay);
+ });
+});
diff --git a/e2e/src/tests/regression/contact-form.regression.spec.ts b/e2e/src/tests/regression/contact-form.regression.spec.ts
new file mode 100644
index 0000000..f1fda10
--- /dev/null
+++ b/e2e/src/tests/regression/contact-form.regression.spec.ts
@@ -0,0 +1,239 @@
+import { test, expect } from '../../fixtures/base.fixture';
+
+test.describe('联系表单回归测试 @regression', () => {
+ test.beforeEach(async ({ contactPage }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+ });
+
+ test('应该能够提交完整的表单', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillAndSubmitForm(formData);
+ await contactPage.waitForFormSubmission();
+ const isSubmitted = await contactPage.isFormSubmitted();
+ expect(isSubmitted).toBe(true);
+ });
+
+ test('应该验证必填字段', async ({ contactPage }) => {
+ await contactPage.submitForm();
+ await contactPage.waitForFormSubmission();
+ const isSubmitted = await contactPage.isFormSubmitted();
+ expect(isSubmitted).toBe(false);
+ });
+
+ test('应该验证邮箱格式', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ formData.email = testDataGenerator.generateInvalidEmail();
+ await contactPage.fillContactForm(formData);
+ const isValid = await contactPage.isEmailValid();
+ expect(isValid).toBe(false);
+ });
+
+ test('应该能够清空表单', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillContactForm(formData);
+ await contactPage.clearForm();
+
+ const nameValue = await contactPage.getNameInputValue();
+ const emailValue = await contactPage.getEmailInputValue();
+ const subjectValue = await contactPage.getSubjectInputValue();
+ const messageValue = await contactPage.getMessageInputValue();
+
+ expect(nameValue).toBe('');
+ expect(emailValue).toBe('');
+ expect(subjectValue).toBe('');
+ expect(messageValue).toBe('');
+ });
+
+ test('应该能够输入长消息', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ formData.message = testDataGenerator.generateMessage(200, 500);
+ await contactPage.fillContactForm(formData);
+ const messageValue = await contactPage.getMessageInputValue();
+ expect(messageValue).toBe(formData.message);
+ });
+
+ test('应该能够输入特殊字符', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ formData.message = testDataGenerator.generateSpecialCharacters();
+ await contactPage.fillContactForm(formData);
+ const messageValue = await contactPage.getMessageInputValue();
+ expect(messageValue).toBe(formData.message);
+ });
+
+ test('应该能够输入中文字符', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ formData.message = testDataGenerator.generateChineseCharacters();
+ await contactPage.fillContactForm(formData);
+ const messageValue = await contactPage.getMessageInputValue();
+ expect(messageValue).toBe(formData.message);
+ });
+
+ test('应该能够输入混合内容', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ formData.message = testDataGenerator.generateMixedContent();
+ await contactPage.fillContactForm(formData);
+ const messageValue = await contactPage.getMessageInputValue();
+ expect(messageValue).toBe(formData.message);
+ });
+
+ test('应该能够聚焦和失焦字段', async ({ contactPage }) => {
+ await contactPage.focusOnField('name');
+ const isNameFocused = await contactPage.nameInput.evaluate(el => document.activeElement === el);
+ expect(isNameFocused).toBe(true);
+
+ await contactPage.blurField('name');
+ const isNameBlurred = await contactPage.nameInput.evaluate(el => document.activeElement !== el);
+ expect(isNameBlurred).toBe(true);
+ });
+
+ test('应该能够使用键盘导航表单', async ({ contactPage }) => {
+ await contactPage.nameInput.focus();
+ await contactPage.page.keyboard.press('Tab');
+ await contactPage.page.waitForTimeout(100);
+ const focusedElement = await contactPage.page.evaluate(() => document.activeElement?.tagName);
+ expect(focusedElement).toBe('INPUT');
+ });
+
+ test('应该能够使用回车键提交表单', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillContactForm(formData);
+ await contactPage.messageInput.press('Enter');
+ await contactPage.waitForFormSubmission();
+ const isSubmitted = await contactPage.isFormSubmitted();
+ expect(isSubmitted).toBe(true);
+ });
+
+ test('应该显示提交按钮的加载状态', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillContactForm(formData);
+ await contactPage.submitButton.click();
+ await contactPage.page.waitForTimeout(500);
+ const isLoading = await contactPage.isSubmitButtonLoading();
+ expect(isLoading).toBe(true);
+ });
+
+ test('应该显示成功消息', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillAndSubmitForm(formData);
+ await contactPage.waitForFormSubmission();
+ const isSuccessVisible = await contactPage.isSuccessMessageVisible();
+ expect(isSuccessVisible).toBe(true);
+ });
+
+ test('应该显示正确的成功消息文本', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillAndSubmitForm(formData);
+ await contactPage.waitForFormSubmission();
+ const successText = await contactPage.getSuccessMessageText();
+ expect(successText).toContain('消息已发送');
+ });
+
+ test('应该能够重新提交表单', async ({ contactPage, testDataGenerator }) => {
+ const formData1 = testDataGenerator.generateContactFormData();
+ await contactPage.fillAndSubmitForm(formData1);
+ await contactPage.waitForFormSubmission();
+
+ await contactPage.page.reload();
+ await contactPage.waitForPageLoad();
+
+ const formData2 = testDataGenerator.generateContactFormData();
+ await contactPage.fillAndSubmitForm(formData2);
+ await contactPage.waitForFormSubmission();
+ const isSubmitted = await contactPage.isFormSubmitted();
+ expect(isSubmitted).toBe(true);
+ });
+
+ test('应该能够输入空格', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ formData.message = ' 测试消息 ';
+ await contactPage.fillContactForm(formData);
+ const messageValue = await contactPage.getMessageInputValue();
+ expect(messageValue).toBe(' 测试消息 ');
+ });
+
+ test('应该能够输入换行符', async ({ contactPage }) => {
+ const message = '第一行\n第二行\n第三行';
+ await contactPage.messageInput.fill(message);
+ const messageValue = await contactPage.getMessageInputValue();
+ expect(messageValue).toBe(message);
+ });
+
+ test('应该能够输入URL', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ formData.message = `请查看我的网站:${testDataGenerator.generateUrl()}`;
+ await contactPage.fillContactForm(formData);
+ const messageValue = await contactPage.getMessageInputValue();
+ expect(messageValue).toContain('http');
+ });
+
+ test('应该能够输入数字', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ formData.phone = testDataGenerator.generatePhone();
+ await contactPage.fillContactForm(formData);
+ const phoneValue = await contactPage.getPhoneInputValue();
+ expect(phoneValue).toBe(formData.phone);
+ });
+
+ test('应该能够输入电子邮件地址', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillContactForm(formData);
+ const emailValue = await contactPage.getEmailInputValue();
+ expect(emailValue).toContain('@');
+ expect(emailValue).toContain('.');
+ });
+
+ test('应该能够截取表单截图', async ({ contactPage }) => {
+ await contactPage.scrollToForm();
+ const isVisible = await contactPage.contactForm.isVisible();
+ expect(isVisible).toBe(true);
+ });
+
+ test('应该能够截取成功消息截图', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillAndSubmitForm(formData);
+ await contactPage.waitForFormSubmission();
+ const isSuccessVisible = await contactPage.isSuccessMessageVisible();
+ expect(isSuccessVisible).toBe(true);
+ });
+
+ test('应该正确处理表单重置', async ({ contactPage, testDataGenerator }) => {
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillContactForm(formData);
+ await contactPage.page.reload();
+ await contactPage.waitForPageLoad();
+
+ const nameValue = await contactPage.getNameInputValue();
+ const emailValue = await contactPage.getEmailInputValue();
+ const subjectValue = await contactPage.getSubjectInputValue();
+ const messageValue = await contactPage.getMessageInputValue();
+
+ expect(nameValue).toBe('');
+ expect(emailValue).toBe('');
+ expect(subjectValue).toBe('');
+ expect(messageValue).toBe('');
+ });
+
+ test('应该没有JavaScript错误', async ({ contactPage, page }) => {
+ const errors: string[] = [];
+ page.on('pageerror', error => {
+ errors.push(error.toString());
+ });
+ await contactPage.waitForPageLoad();
+ const formData = { name: '测试', email: 'test@example.com', subject: '测试', message: '测试消息' };
+ await contactPage.fillContactForm(formData);
+ await contactPage.submitForm();
+ await contactPage.waitForFormSubmission();
+ expect(errors.length).toBe(0);
+ });
+
+ test('应该正确处理网络请求', async ({ contactPage, page }) => {
+ const failedRequests: string[] = [];
+ page.on('requestfailed', request => {
+ failedRequests.push(request.url());
+ });
+ await contactPage.waitForPageLoad();
+ await contactPage.page.waitForTimeout(2000);
+ expect(failedRequests.length).toBe(0);
+ });
+});
diff --git a/e2e/src/tests/regression/home-page.regression.spec.ts b/e2e/src/tests/regression/home-page.regression.spec.ts
new file mode 100644
index 0000000..0298fa2
--- /dev/null
+++ b/e2e/src/tests/regression/home-page.regression.spec.ts
@@ -0,0 +1,213 @@
+import { test, expect } from '../../fixtures/base.fixture';
+
+test.describe('首页回归测试 @regression', () => {
+ test.beforeEach(async ({ homePage }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ });
+
+ test('应该正确响应滚动事件', async ({ homePage }) => {
+ const initialBg = await homePage.getHeaderBackgroundColor();
+ await homePage.scrollToSection('services');
+ await homePage.page.waitForTimeout(500);
+ const scrolledBg = await homePage.getHeaderBackgroundColor();
+ expect(scrolledBg).not.toBe(initialBg);
+ });
+
+ test('应该显示粘性头部', async ({ homePage }) => {
+ const isSticky = await homePage.isHeaderSticky();
+ expect(isSticky).toBe(true);
+ });
+
+ test('滚动后应该显示头部阴影', async ({ homePage }) => {
+ await homePage.scrollToSection('services');
+ await homePage.page.waitForTimeout(500);
+ const hasShadow = await homePage.isHeaderScrolled();
+ expect(hasShadow).toBe(true);
+ });
+
+ test('应该能够通过导航跳转到各个区块', async ({ homePage }) => {
+ const labels = await homePage.getAllNavigationLabels();
+ for (let i = 0; i < Math.min(labels.length, 3); i++) {
+ await homePage.clickNavigationItem(labels[i]);
+ await homePage.page.waitForTimeout(1000);
+ const url = homePage.page.url();
+ expect(url).toContain('#');
+ }
+ });
+
+ test('应该正确高亮当前区块的导航项', async ({ homePage }) => {
+ await homePage.scrollToSection('services');
+ await homePage.page.waitForTimeout(500);
+ const activeItem = await homePage.getActiveNavigationItem();
+ expect(activeItem).toBeTruthy();
+ });
+
+ test('应该能够点击Logo返回首页', async ({ homePage }) => {
+ await homePage.scrollToSection('services');
+ await homePage.logo.click();
+ await homePage.page.waitForTimeout(1000);
+ const url = homePage.page.url();
+ expect(url).toMatch(/\/$/);
+ });
+
+ test('应该能够通过立即咨询按钮跳转到联系页面', async ({ homePage }) => {
+ await homePage.clickContactButton();
+ await homePage.page.waitForTimeout(1000);
+ const url = homePage.page.url();
+ expect(url).toContain('/contact');
+ });
+
+ test('应该能够打开和关闭移动端菜单', async ({ homePage }) => {
+ await homePage.openMobileMenu();
+ await expect(homePage.mobileMenu).toBeVisible();
+
+ await homePage.closeMobileMenu();
+ await expect(homePage.mobileMenu).not.toBeVisible();
+ });
+
+ test('移动端菜单应该包含所有导航项', async ({ homePage }) => {
+ await homePage.openMobileMenu();
+ const desktopNavItems = await homePage.getAllNavigationLabels();
+ const mobileNavItems = homePage.mobileMenu.locator('a');
+ const mobileCount = await mobileNavItems.count();
+ expect(mobileCount).toBeGreaterThan(0);
+ expect(mobileCount).toBe(desktopNavItems.length);
+ });
+
+ test('应该能够通过移动端菜单导航', async ({ homePage }) => {
+ await homePage.openMobileMenu();
+ const firstLink = homePage.mobileMenu.locator('a').first();
+ await firstLink.click();
+ await homePage.page.waitForTimeout(1000);
+ const isMenuVisible = await homePage.mobileMenu.isVisible();
+ expect(isMenuVisible).toBe(false);
+ });
+
+ test('应该能够平滑滚动到各个区块', async ({ homePage }) => {
+ const sections = ['services', 'products', 'cases'];
+ for (const sectionId of sections) {
+ await homePage.scrollToSection(sectionId);
+ const isVisible = await homePage.isSectionVisible(sectionId);
+ expect(isVisible).toBe(true);
+ }
+ });
+
+ test('应该正确显示所有区块标题', async ({ homePage }) => {
+ const titles = [
+ await homePage.getHeroSectionTitle(),
+ await homePage.getServicesSectionTitle(),
+ await homePage.getProductsSectionTitle(),
+ await homePage.getCasesSectionTitle(),
+ await homePage.getAboutSectionTitle(),
+ await homePage.getNewsSectionTitle(),
+ await homePage.getContactSectionTitle(),
+ ];
+ titles.forEach(title => {
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ });
+ });
+
+ test('应该能够滚动到页面底部并返回顶部', async ({ homePage }) => {
+ await homePage.scrollToBottom();
+ const bottomScroll = await homePage.page.evaluate(() => window.scrollY);
+ expect(bottomScroll).toBeGreaterThan(0);
+
+ await homePage.scrollToTop();
+ const topScroll = await homePage.page.evaluate(() => window.scrollY);
+ expect(topScroll).toBe(0);
+ });
+
+ test('应该正确处理快速滚动', async ({ homePage }) => {
+ for (let i = 0; i < 5; i++) {
+ await homePage.page.evaluate(() => window.scrollBy(0, 500));
+ await homePage.page.waitForTimeout(100);
+ }
+ const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
+ expect(scrollPosition).toBeGreaterThan(0);
+ });
+
+ test('应该能够截取各个区块的截图', async ({ homePage }) => {
+ const sections = ['home', 'services'];
+ for (const sectionId of sections) {
+ await homePage.scrollToSection(sectionId);
+ await homePage.page.waitForTimeout(500);
+ const isVisible = await homePage.isSectionVisible(sectionId);
+ expect(isVisible).toBe(true);
+ }
+ });
+
+ test('应该正确响应窗口大小变化', async ({ homePage }) => {
+ await homePage.page.setViewportSize({ width: 768, height: 1024 });
+ await homePage.page.waitForTimeout(500);
+ await expect(homePage.header).toBeVisible();
+
+ await homePage.page.setViewportSize({ width: 1280, height: 720 });
+ await homePage.page.waitForTimeout(500);
+ await expect(homePage.header).toBeVisible();
+ });
+
+ test('应该能够通过键盘导航', async ({ homePage }) => {
+ await homePage.page.keyboard.press('Tab');
+ await homePage.page.waitForTimeout(100);
+ const focusedElement = await homePage.page.evaluate(() => document.activeElement?.tagName);
+ expect(focusedElement).toBeTruthy();
+ });
+
+ test('应该正确显示页脚内容', async ({ homePage }) => {
+ await homePage.scrollToBottom();
+ const footerText = await homePage.getFooterText();
+ expect(footerText).toBeTruthy();
+ expect(footerText.length).toBeGreaterThan(0);
+ });
+
+ test('应该能够重新加载页面', async ({ homePage }) => {
+ await homePage.reload();
+ await homePage.waitForPageLoad();
+ await expect(homePage.header).toBeVisible();
+ await expect(homePage.heroSection).toBeVisible();
+ });
+
+ test('应该能够使用浏览器后退按钮', async ({ homePage }) => {
+ await homePage.clickContactButton();
+ await homePage.page.waitForTimeout(1000);
+ await homePage.goBack();
+ await homePage.page.waitForTimeout(1000);
+ const url = homePage.page.url();
+ expect(url).toMatch(/\/$/);
+ });
+
+ test('应该能够使用浏览器前进按钮', async ({ homePage }) => {
+ await homePage.clickContactButton();
+ await homePage.page.waitForTimeout(1000);
+ await homePage.goBack();
+ await homePage.page.waitForTimeout(1000);
+ await homePage.goForward();
+ await homePage.page.waitForTimeout(1000);
+ const url = homePage.page.url();
+ expect(url).toContain('/contact');
+ });
+
+ test('应该没有JavaScript错误', async ({ homePage, page }) => {
+ const errors: string[] = [];
+ page.on('pageerror', error => {
+ errors.push(error.toString());
+ });
+ await homePage.waitForPageLoad();
+ await homePage.scrollToSection('services');
+ await homePage.scrollToSection('products');
+ await homePage.scrollToSection('cases');
+ expect(errors.length).toBe(0);
+ });
+
+ test('应该正确处理网络请求', async ({ homePage, page }) => {
+ const failedRequests: string[] = [];
+ page.on('requestfailed', request => {
+ failedRequests.push(request.url());
+ });
+ await homePage.waitForPageLoad();
+ await homePage.page.waitForTimeout(2000);
+ expect(failedRequests.length).toBe(0);
+ });
+});
diff --git a/e2e/src/tests/responsive/mobile-interaction.spec.ts b/e2e/src/tests/responsive/mobile-interaction.spec.ts
new file mode 100644
index 0000000..dcde264
--- /dev/null
+++ b/e2e/src/tests/responsive/mobile-interaction.spec.ts
@@ -0,0 +1,356 @@
+import { test, expect } from '../../fixtures/base.fixture';
+import { devices, getMobileDevices } from '../../utils/devices';
+
+test.describe('移动端交互测试 @responsive', () => {
+ for (const device of getMobileDevices()) {
+ test(`移动端 ${device.name} - 应该能够打开和关闭菜单`, async ({ homePage, page }) => {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.mobileMenuButton).toBeVisible();
+
+ await homePage.openMobileMenu();
+ await expect(homePage.mobileMenu).toBeVisible();
+
+ await homePage.closeMobileMenu();
+ await expect(homePage.mobileMenu).not.toBeVisible();
+ });
+ }
+
+ test('移动端 - 应该能够通过菜单导航', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.openMobileMenu();
+ const firstLink = homePage.mobileMenu.locator('a').first();
+ await firstLink.click();
+ await homePage.page.waitForTimeout(1000);
+
+ const isMenuVisible = await homePage.mobileMenu.isVisible();
+ expect(isMenuVisible).toBe(false);
+ });
+
+ test('移动端 - 应该能够滚动页面', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const initialScroll = await homePage.page.evaluate(() => window.scrollY);
+ expect(initialScroll).toBe(0);
+
+ await homePage.scrollToSection('services');
+ const afterScroll = await homePage.page.evaluate(() => window.scrollY);
+ expect(afterScroll).toBeGreaterThan(0);
+ });
+
+ test('移动端 - 应该能够点击Logo', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.logo.click();
+ await homePage.page.waitForTimeout(1000);
+
+ const url = homePage.page.url();
+ expect(url).toMatch(/\/$/);
+ });
+
+ test('移动端 - 应该能够点击联系按钮', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.openMobileMenu();
+ const contactButton = homePage.mobileMenu.locator('a:has-text("联系我们")').first();
+ await contactButton.click();
+ await homePage.page.waitForTimeout(1000);
+
+ const url = homePage.page.url();
+ expect(url).toContain('/contact');
+ });
+
+ test('移动端 - 应该能够快速滚动', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const startTime = Date.now();
+ for (let i = 0; i < 10; i++) {
+ await homePage.page.evaluate(() => window.scrollBy(0, 200));
+ await homePage.page.waitForTimeout(50);
+ }
+ const endTime = Date.now();
+
+ const scrollDuration = endTime - startTime;
+ expect(scrollDuration).toBeLessThan(1500);
+ });
+
+ test('移动端 - 应该能够触摸交互', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const heroSection = homePage.heroSection;
+ await heroSection.tap();
+ await homePage.page.waitForTimeout(500);
+
+ const isVisible = await heroSection.isVisible();
+ expect(isVisible).toBe(true);
+ });
+
+ test('移动端 - 应该能够滑动页面', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const heroSection = homePage.heroSection;
+ await heroSection.tap();
+
+ const startX = await homePage.page.evaluate(() => window.scrollX);
+ const startY = await homePage.page.evaluate(() => window.scrollY);
+
+ await homePage.page.touchscreen.tap(200, 500);
+ await homePage.page.waitForTimeout(500);
+
+ const endX = await homePage.page.evaluate(() => window.scrollX);
+ const endY = await homePage.page.evaluate(() => window.scrollY);
+
+ expect(endY).toBeGreaterThan(startY);
+ });
+
+ test('移动端 - 应该能够双击缩放', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const heroSection = homePage.heroSection;
+ await heroSection.tap();
+ await homePage.page.waitForTimeout(200);
+ await heroSection.tap();
+ await homePage.page.waitForTimeout(500);
+
+ const isVisible = await heroSection.isVisible();
+ expect(isVisible).toBe(true);
+ });
+
+ test('移动端 - 应该能够长按', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const heroSection = homePage.heroSection;
+ await heroSection.tap();
+ await homePage.page.waitForTimeout(1000);
+
+ const isVisible = await heroSection.isVisible();
+ expect(isVisible).toBe(true);
+ });
+
+ test('移动端 - 应该能够使用键盘导航', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.page.keyboard.press('Tab');
+ await homePage.page.waitForTimeout(100);
+
+ const focusedElement = await homePage.page.evaluate(() => document.activeElement?.tagName);
+ expect(focusedElement).toBeTruthy();
+ });
+
+ test('移动端 - 应该能够输入表单', async ({ contactPage, page, testDataGenerator }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillContactForm(formData);
+
+ const nameValue = await contactPage.getNameInputValue();
+ const emailValue = await contactPage.getEmailInputValue();
+
+ expect(nameValue).toBe(formData.name);
+ expect(emailValue).toBe(formData.email);
+ });
+
+ test('移动端 - 应该能够提交表单', async ({ contactPage, page, testDataGenerator }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillAndSubmitForm(formData);
+ await contactPage.waitForFormSubmission();
+
+ const isSubmitted = await contactPage.isFormSubmitted();
+ expect(isSubmitted).toBe(true);
+ });
+
+ test('移动端 - 应该能够滚动到表单', async ({ contactPage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await contactPage.scrollToForm();
+ const isVisible = await contactPage.contactForm.isVisible();
+ expect(isVisible).toBe(true);
+ });
+
+ test('移动端 - 应该能够点击表单字段', async ({ contactPage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await contactPage.nameInput.tap();
+ await homePage.page.waitForTimeout(100);
+
+ const isFocused = await contactPage.nameInput.evaluate(el => document.activeElement === el);
+ expect(isFocused).toBe(true);
+ });
+
+ test('移动端 - 应该能够使用返回按钮', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.clickContactButton();
+ await homePage.page.waitForTimeout(1000);
+
+ await homePage.goBack();
+ await homePage.page.waitForTimeout(1000);
+
+ const url = homePage.page.url();
+ expect(url).toMatch(/\/$/);
+ });
+
+ test('移动端 - 应该能够使用前进按钮', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.clickContactButton();
+ await homePage.page.waitForTimeout(1000);
+ await homePage.goBack();
+ await homePage.page.waitForTimeout(1000);
+ await homePage.goForward();
+ await homePage.page.waitForTimeout(1000);
+
+ const url = homePage.page.url();
+ expect(url).toContain('/contact');
+ });
+
+ test('移动端 - 应该能够刷新页面', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.reload();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.header).toBeVisible();
+ await expect(homePage.heroSection).toBeVisible();
+ });
+
+ test('移动端 - 应该没有JavaScript错误', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+
+ const errors: string[] = [];
+ page.on('pageerror', error => {
+ errors.push(error.toString());
+ });
+
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToSection('services');
+ await homePage.scrollToSection('products');
+
+ expect(errors.length).toBe(0);
+ });
+
+ test('移动端 - 应该能够处理快速连续点击', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.openMobileMenu();
+
+ const startTime = Date.now();
+ const links = homePage.mobileMenu.locator('a');
+ const count = await links.count();
+
+ for (let i = 0; i < Math.min(count, 3); i++) {
+ await links.nth(i).tap();
+ await homePage.page.waitForTimeout(200);
+ }
+ const endTime = Date.now();
+
+ const rapidClickDuration = endTime - startTime;
+ expect(rapidClickDuration).toBeLessThan(2000);
+ });
+
+ test('移动端 - 应该能够处理快速滚动', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const startTime = Date.now();
+ for (let i = 0; i < 20; i++) {
+ await homePage.page.evaluate(() => window.scrollBy(0, 100));
+ await homePage.page.waitForTimeout(30);
+ }
+ const endTime = Date.now();
+
+ const rapidScrollDuration = endTime - startTime;
+ expect(rapidScrollDuration).toBeLessThan(1500);
+ });
+
+ test('移动端 - 应该能够处理方向变化', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await page.setViewportSize({ width: 667, height: 375 });
+ await homePage.page.waitForTimeout(500);
+
+ await expect(homePage.header).toBeVisible();
+ await expect(homePage.heroSection).toBeVisible();
+ });
+
+ test('移动端 - 应该能够处理键盘弹出', async ({ contactPage, page, testDataGenerator }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.nameInput.tap();
+ await contactPage.nameInput.fill(formData.name);
+ await homePage.page.waitForTimeout(500);
+
+ const nameValue = await contactPage.getNameInputValue();
+ expect(nameValue).toBe(formData.name);
+ });
+});
diff --git a/e2e/src/tests/responsive/responsive.spec.ts b/e2e/src/tests/responsive/responsive.spec.ts
new file mode 100644
index 0000000..d101121
--- /dev/null
+++ b/e2e/src/tests/responsive/responsive.spec.ts
@@ -0,0 +1,339 @@
+import { test, expect } from '../../fixtures/base.fixture';
+import { devices, getDesktopDevices, getMobileDevices, getTabletDevices } from '../../utils/devices';
+
+test.describe('响应式测试 @responsive', () => {
+ for (const device of getDesktopDevices()) {
+ test(`桌面端 ${device.name} - 首页应该正常显示`, async ({ homePage, page }) => {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.header).toBeVisible();
+ await expect(homePage.heroSection).toBeVisible();
+ await expect(homePage.footer).toBeVisible();
+ await expect(homePage.navigation).toBeVisible();
+ await expect(homePage.mobileMenuButton).not.toBeVisible();
+ });
+ }
+
+ for (const device of getMobileDevices()) {
+ test(`移动端 ${device.name} - 首页应该正常显示`, async ({ homePage, page }) => {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.header).toBeVisible();
+ await expect(homePage.heroSection).toBeVisible();
+ await expect(homePage.footer).toBeVisible();
+ await expect(homePage.mobileMenuButton).toBeVisible();
+ });
+ }
+
+ for (const device of getTabletDevices()) {
+ test(`平板端 ${device.name} - 首页应该正常显示`, async ({ homePage, page }) => {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.header).toBeVisible();
+ await expect(homePage.heroSection).toBeVisible();
+ await expect(homePage.footer).toBeVisible();
+ await expect(homePage.mobileMenuButton).toBeVisible();
+ });
+ }
+
+ test('桌面端 - 导航应该水平显示', async ({ homePage, page }) => {
+ const device = devices['desktop-1280x720'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.navigation).toBeVisible();
+ await expect(homePage.mobileMenuButton).not.toBeVisible();
+
+ const navItems = await homePage.getNavigationItemCount();
+ expect(navItems).toBeGreaterThan(0);
+ });
+
+ test('移动端 - 导航应该隐藏在汉堡菜单中', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.mobileMenuButton).toBeVisible();
+ await expect(homePage.navigation).not.toBeVisible();
+
+ await homePage.openMobileMenu();
+ await expect(homePage.mobileMenu).toBeVisible();
+ });
+
+ test('平板端 - 导航应该隐藏在汉堡菜单中', async ({ homePage, page }) => {
+ const device = devices['tablet-768x1024'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.mobileMenuButton).toBeVisible();
+ await expect(homePage.navigation).not.toBeVisible();
+
+ await homePage.openMobileMenu();
+ await expect(homePage.mobileMenu).toBeVisible();
+ });
+
+ test('所有设备 - 页面应该能够滚动', async ({ homePage, page }) => {
+ const testDevices = [
+ devices['desktop-1280x720'],
+ devices['mobile-375x667'],
+ devices['tablet-768x1024'],
+ ];
+
+ for (const device of testDevices) {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.scrollToSection('services');
+ const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
+ expect(scrollPosition).toBeGreaterThan(0);
+
+ await homePage.scrollToTop();
+ const topPosition = await homePage.page.evaluate(() => window.scrollY);
+ expect(topPosition).toBe(0);
+ }
+ });
+
+ test('所有设备 - 所有区块应该可见', async ({ homePage, page }) => {
+ const testDevices = [
+ devices['desktop-1280x720'],
+ devices['mobile-375x667'],
+ devices['tablet-768x1024'],
+ ];
+
+ const sections = ['services', 'products', 'cases', 'about', 'news', 'contact'];
+
+ for (const device of testDevices) {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ for (const sectionId of sections) {
+ await homePage.scrollToSection(sectionId);
+ const isVisible = await homePage.isSectionVisible(sectionId);
+ expect(isVisible).toBe(true);
+ }
+ }
+ });
+
+ test('移动端 - 移动菜单应该能够打开和关闭', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.openMobileMenu();
+ await expect(homePage.mobileMenu).toBeVisible();
+
+ await homePage.closeMobileMenu();
+ await expect(homePage.mobileMenu).not.toBeVisible();
+ });
+
+ test('桌面端 - Logo应该可见', async ({ homePage, page }) => {
+ const device = devices['desktop-1280x720'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.logo).toBeVisible();
+ const altText = await homePage.getLogoAltText();
+ expect(altText).toBeTruthy();
+ });
+
+ test('移动端 - Logo应该可见', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.logo).toBeVisible();
+ const altText = await homePage.getLogoAltText();
+ expect(altText).toBeTruthy();
+ });
+
+ test('所有设备 - 页脚应该可见', async ({ homePage, page }) => {
+ const testDevices = [
+ devices['desktop-1280x720'],
+ devices['mobile-375x667'],
+ devices['tablet-768x1024'],
+ ];
+
+ for (const device of testDevices) {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.scrollToBottom();
+ await expect(homePage.footer).toBeVisible();
+ }
+ });
+
+ test('桌面端 - 立即咨询按钮应该可见', async ({ homePage, page }) => {
+ const device = devices['desktop-1280x720'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const contactButton = homePage.page.locator('a:has-text("立即咨询")').first();
+ await expect(contactButton).toBeVisible();
+ });
+
+ test('移动端 - 立即咨询按钮应该可见', async ({ homePage, page }) => {
+ const device = devices['mobile-375x667'];
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.openMobileMenu();
+ const contactButton = homePage.mobileMenu.locator('a:has-text("联系我们")').first();
+ await expect(contactButton).toBeVisible();
+ });
+
+ test('所有设备 - 应该能够点击导航项', async ({ homePage, page }) => {
+ const testDevices = [
+ devices['desktop-1280x720'],
+ devices['mobile-375x667'],
+ devices['tablet-768x1024'],
+ ];
+
+ for (const device of testDevices) {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ if (device.isMobile) {
+ await homePage.openMobileMenu();
+ }
+
+ const labels = await homePage.getAllNavigationLabels();
+ if (labels.length > 0) {
+ await homePage.clickNavigationItem(labels[0]);
+ await homePage.page.waitForTimeout(1000);
+ }
+ }
+ });
+
+ test('所有设备 - 应该能够点击Logo返回首页', async ({ homePage, page }) => {
+ const testDevices = [
+ devices['desktop-1280x720'],
+ devices['mobile-375x667'],
+ devices['tablet-768x1024'],
+ ];
+
+ for (const device of testDevices) {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.logo.click();
+ await homePage.page.waitForTimeout(1000);
+ const url = homePage.page.url();
+ expect(url).toMatch(/\/$/);
+ }
+ });
+
+ test('所有设备 - 应该没有水平滚动条', async ({ homePage, page }) => {
+ const testDevices = [
+ devices['desktop-1280x720'],
+ devices['mobile-375x667'],
+ devices['tablet-768x1024'],
+ ];
+
+ for (const device of testDevices) {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const hasHorizontalScroll = await homePage.page.evaluate(() => {
+ return document.body.scrollWidth > document.body.clientWidth;
+ });
+
+ expect(hasHorizontalScroll).toBe(false);
+ }
+ });
+
+ test('所有设备 - 文字应该可读', async ({ homePage, page }) => {
+ const testDevices = [
+ devices['desktop-1280x720'],
+ devices['mobile-375x667'],
+ devices['tablet-768x1024'],
+ ];
+
+ for (const device of testDevices) {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const heroTitle = await homePage.getHeroSectionTitle();
+ expect(heroTitle).toBeTruthy();
+ expect(heroTitle.length).toBeGreaterThan(0);
+ }
+ });
+
+ test('所有设备 - 应该能够滚动到页面底部', async ({ homePage, page }) => {
+ const testDevices = [
+ devices['desktop-1280x720'],
+ devices['mobile-375x667'],
+ devices['tablet-768x1024'],
+ ];
+
+ for (const device of testDevices) {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.scrollToBottom();
+ const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
+ expect(scrollPosition).toBeGreaterThan(0);
+ }
+ });
+
+ test('所有设备 - 应该能够滚动到页面顶部', async ({ homePage, page }) => {
+ const testDevices = [
+ devices['desktop-1280x720'],
+ devices['mobile-375x667'],
+ devices['tablet-768x1024'],
+ ];
+
+ for (const device of testDevices) {
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await homePage.scrollToBottom();
+ await homePage.scrollToTop();
+ const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
+ expect(scrollPosition).toBe(0);
+ }
+ });
+
+ test('所有设备 - 页面应该没有JavaScript错误', async ({ homePage, page }) => {
+ const testDevices = [
+ devices['desktop-1280x720'],
+ devices['mobile-375x667'],
+ devices['tablet-768x1024'],
+ ];
+
+ for (const device of testDevices) {
+ const errors: string[] = [];
+ page.on('pageerror', error => {
+ errors.push(error.toString());
+ });
+
+ await page.setViewportSize(device.viewport);
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ expect(errors.length).toBe(0);
+ }
+ });
+});
diff --git a/e2e/src/tests/smoke/contact-page.smoke.spec.ts b/e2e/src/tests/smoke/contact-page.smoke.spec.ts
new file mode 100644
index 0000000..1bfd72a
--- /dev/null
+++ b/e2e/src/tests/smoke/contact-page.smoke.spec.ts
@@ -0,0 +1,173 @@
+import { test, expect } from '../../fixtures/base.fixture';
+
+test.describe('联系页面冒烟测试 @smoke', () => {
+ test.beforeEach(async ({ contactPage }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+ });
+
+ test('应该成功加载联系页面', async ({ contactPage }) => {
+ await expect(contactPage.page).toHaveURL(/\/contact/);
+ await expect(contactPage.pageHeader).toBeVisible();
+ await expect(contactPage.contactForm).toBeVisible();
+ });
+
+ test('应该显示正确的页面标题', async ({ contactPage }) => {
+ const title = await contactPage.getPageTitle();
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ expect(title).toContain('联系');
+ });
+
+ test('应该显示页面描述', async ({ contactPage }) => {
+ const description = await contactPage.getPageDescription();
+ expect(description).toBeTruthy();
+ expect(description.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示页面徽章', async ({ contactPage }) => {
+ const badge = await contactPage.getBadgeText();
+ expect(badge).toBeTruthy();
+ expect(badge).toBe('联系我们');
+ });
+
+ test('应该显示联系表单', async ({ contactPage }) => {
+ await expect(contactPage.contactForm).toBeVisible();
+ await expect(contactPage.nameInput).toBeVisible();
+ await expect(contactPage.emailInput).toBeVisible();
+ await expect(contactPage.subjectInput).toBeVisible();
+ await expect(contactPage.messageInput).toBeVisible();
+ await expect(contactPage.submitButton).toBeVisible();
+ });
+
+ test('应该显示所有表单字段', async ({ contactPage }) => {
+ await expect(contactPage.nameInput).toBeVisible();
+ await expect(contactPage.phoneInput).toBeVisible();
+ await expect(contactPage.emailInput).toBeVisible();
+ await expect(contactPage.subjectInput).toBeVisible();
+ await expect(contactPage.messageInput).toBeVisible();
+ });
+
+ test('应该显示联系信息卡片', async ({ contactPage }) => {
+ await expect(contactPage.contactInfoCard).toBeVisible();
+ await expect(contactPage.addressInfo).toBeVisible();
+ await expect(contactPage.phoneInfo).toBeVisible();
+ await expect(contactPage.emailInfo).toBeVisible();
+ });
+
+ test('应该显示工作时间卡片', async ({ contactPage }) => {
+ await expect(contactPage.workHoursCard).toBeVisible();
+ const workHours = await contactPage.getWorkHours();
+ expect(workHours.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示公司地址', async ({ contactPage }) => {
+ const address = await contactPage.getAddress();
+ expect(address).toBeTruthy();
+ expect(address.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示联系电话', async ({ contactPage }) => {
+ const phone = await contactPage.getPhone();
+ expect(phone).toBeTruthy();
+ expect(phone.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示电子邮箱', async ({ contactPage }) => {
+ const email = await contactPage.getEmail();
+ expect(email).toBeTruthy();
+ expect(email.length).toBeGreaterThan(0);
+ expect(email).toContain('@');
+ });
+
+ test('应该显示提交按钮', async ({ contactPage }) => {
+ await expect(contactPage.submitButton).toBeVisible();
+ const buttonText = await contactPage.getSubmitButtonText();
+ expect(buttonText).toContain('发送');
+ });
+
+ test('应该能够输入姓名', async ({ contactPage }) => {
+ const testName = '测试用户';
+ await contactPage.nameInput.fill(testName);
+ const value = await contactPage.getNameInputValue();
+ expect(value).toBe(testName);
+ });
+
+ test('应该能够输入电话', async ({ contactPage }) => {
+ const testPhone = '13800138000';
+ await contactPage.phoneInput.fill(testPhone);
+ const value = await contactPage.getPhoneInputValue();
+ expect(value).toBe(testPhone);
+ });
+
+ test('应该能够输入邮箱', async ({ contactPage }) => {
+ const testEmail = 'test@example.com';
+ await contactPage.emailInput.fill(testEmail);
+ const value = await contactPage.getEmailInputValue();
+ expect(value).toBe(testEmail);
+ });
+
+ test('应该能够输入主题', async ({ contactPage }) => {
+ const testSubject = '测试主题';
+ await contactPage.subjectInput.fill(testSubject);
+ const value = await contactPage.getSubjectInputValue();
+ expect(value).toBe(testSubject);
+ });
+
+ test('应该能够输入消息内容', async ({ contactPage }) => {
+ const testMessage = '这是一条测试消息';
+ await contactPage.messageInput.fill(testMessage);
+ const value = await contactPage.getMessageInputValue();
+ expect(value).toBe(testMessage);
+ });
+
+ test('应该显示必填字段标记', async ({ contactPage }) => {
+ const isNameRequired = await contactPage.isFieldRequired('name');
+ const isEmailRequired = await contactPage.isFieldRequired('email');
+ const isSubjectRequired = await contactPage.isFieldRequired('subject');
+ const isMessageRequired = await contactPage.isFieldRequired('message');
+
+ expect(isNameRequired).toBe(true);
+ expect(isEmailRequired).toBe(true);
+ expect(isSubjectRequired).toBe(true);
+ expect(isMessageRequired).toBe(true);
+ });
+
+ test('应该显示字段占位符', async ({ contactPage }) => {
+ const namePlaceholder = await contactPage.getFieldPlaceholder('name');
+ const emailPlaceholder = await contactPage.getFieldPlaceholder('email');
+ const subjectPlaceholder = await contactPage.getFieldPlaceholder('subject');
+ const messagePlaceholder = await contactPage.getFieldPlaceholder('message');
+
+ expect(namePlaceholder).toBeTruthy();
+ expect(emailPlaceholder).toBeTruthy();
+ expect(subjectPlaceholder).toBeTruthy();
+ expect(messagePlaceholder).toBeTruthy();
+ });
+
+ test('应该有正确的工作时间信息', async ({ contactPage }) => {
+ const workHours = await contactPage.getWorkHours();
+ expect(workHours.length).toBeGreaterThan(0);
+ workHours.forEach(item => {
+ expect(item.day).toBeTruthy();
+ expect(item.hours).toBeTruthy();
+ });
+ });
+
+ test('应该没有控制台错误', async ({ contactPage, page }) => {
+ const errors: string[] = [];
+ page.on('console', msg => {
+ if (msg.type() === 'error') {
+ errors.push(msg.text());
+ }
+ });
+ await contactPage.waitForPageLoad();
+ expect(errors.length).toBe(0);
+ });
+
+ test('应该能够滚动到表单', async ({ contactPage }) => {
+ await contactPage.scrollToForm();
+ const isVisible = await contactPage.contactForm.isVisible();
+ expect(isVisible).toBe(true);
+ });
+});
diff --git a/e2e/src/tests/smoke/home-page.smoke.spec.ts b/e2e/src/tests/smoke/home-page.smoke.spec.ts
new file mode 100644
index 0000000..c1f89d7
--- /dev/null
+++ b/e2e/src/tests/smoke/home-page.smoke.spec.ts
@@ -0,0 +1,137 @@
+import { test, expect } from '../../fixtures/base.fixture';
+
+test.describe('首页冒烟测试 @smoke', () => {
+ test.beforeEach(async ({ homePage }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ });
+
+ test('应该成功加载首页', async ({ homePage }) => {
+ await expect(homePage.page).toHaveURL(/\/$/);
+ await expect(homePage.header).toBeVisible();
+ await expect(homePage.heroSection).toBeVisible();
+ await expect(homePage.footer).toBeVisible();
+ });
+
+ test('应该显示正确的页面标题', async ({ homePage }) => {
+ const title = await homePage.getTitle();
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示Logo', async ({ homePage }) => {
+ await expect(homePage.logo).toBeVisible();
+ const altText = await homePage.getLogoAltText();
+ expect(altText).toBeTruthy();
+ });
+
+ test('应该显示主导航菜单', async ({ homePage }) => {
+ await expect(homePage.navigation).toBeVisible();
+ const navItems = await homePage.getNavigationItemCount();
+ expect(navItems).toBeGreaterThan(0);
+ });
+
+ test('应该显示所有主要区块', async ({ homePage }) => {
+ await expect(homePage.heroSection).toBeVisible();
+ await homePage.scrollToSection('services');
+ await expect(homePage.servicesSection).toBeVisible();
+ await homePage.scrollToSection('products');
+ await expect(homePage.productsSection).toBeVisible();
+ await homePage.scrollToSection('cases');
+ await expect(homePage.casesSection).toBeVisible();
+ await homePage.scrollToSection('about');
+ await expect(homePage.aboutSection).toBeVisible();
+ await homePage.scrollToSection('news');
+ await expect(homePage.newsSection).toBeVisible();
+ await homePage.scrollToSection('contact');
+ await expect(homePage.contactSection).toBeVisible();
+ });
+
+ test('应该显示页脚', async ({ homePage }) => {
+ await homePage.scrollToBottom();
+ await expect(homePage.footer).toBeVisible();
+ const footerText = await homePage.getFooterText();
+ expect(footerText.length).toBeGreaterThan(0);
+ });
+
+ test('应该能够滚动到页面底部', async ({ homePage }) => {
+ await homePage.scrollToBottom();
+ const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
+ expect(scrollPosition).toBeGreaterThan(0);
+ });
+
+ test('应该能够滚动到页面顶部', async ({ homePage }) => {
+ await homePage.scrollToBottom();
+ await homePage.scrollToTop();
+ const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
+ expect(scrollPosition).toBe(0);
+ });
+
+ test('应该显示Hero区块标题', async ({ homePage }) => {
+ const title = await homePage.getHeroSectionTitle();
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示Services区块标题', async ({ homePage }) => {
+ await homePage.waitForServicesSection();
+ const title = await homePage.getServicesSectionTitle();
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示Products区块标题', async ({ homePage }) => {
+ await homePage.waitForProductsSection();
+ const title = await homePage.getProductsSectionTitle();
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示Cases区块标题', async ({ homePage }) => {
+ await homePage.waitForCasesSection();
+ const title = await homePage.getCasesSectionTitle();
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示About区块标题', async ({ homePage }) => {
+ await homePage.waitForAboutSection();
+ const title = await homePage.getAboutSectionTitle();
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示News区块标题', async ({ homePage }) => {
+ await homePage.waitForNewsSection();
+ const title = await homePage.getNewsSectionTitle();
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ });
+
+ test('应该显示Contact区块标题', async ({ homePage }) => {
+ await homePage.waitForContactSection();
+ const title = await homePage.getContactSectionTitle();
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ });
+
+ test('应该有正确的导航标签', async ({ homePage }) => {
+ const labels = await homePage.getAllNavigationLabels();
+ expect(labels.length).toBeGreaterThan(0);
+ labels.forEach(label => {
+ expect(label).toBeTruthy();
+ expect(label.length).toBeGreaterThan(0);
+ });
+ });
+
+ test('应该没有控制台错误', async ({ homePage, page }) => {
+ const errors: string[] = [];
+ page.on('console', msg => {
+ if (msg.type() === 'error') {
+ errors.push(msg.text());
+ }
+ });
+ await homePage.waitForPageLoad();
+ expect(errors.length).toBe(0);
+ });
+});
diff --git a/e2e/src/tests/smoke/navigation.smoke.spec.ts b/e2e/src/tests/smoke/navigation.smoke.spec.ts
new file mode 100644
index 0000000..ce4d693
--- /dev/null
+++ b/e2e/src/tests/smoke/navigation.smoke.spec.ts
@@ -0,0 +1,131 @@
+import { test, expect } from '../../fixtures/base.fixture';
+
+test.describe('导航冒烟测试 @smoke', () => {
+ test.beforeEach(async ({ homePage }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ });
+
+ test('应该显示主导航菜单', async ({ homePage }) => {
+ await expect(homePage.navigation).toBeVisible();
+ });
+
+ test('应该显示Logo链接', async ({ homePage }) => {
+ await expect(homePage.logo).toBeVisible();
+ const altText = await homePage.getLogoAltText();
+ expect(altText).toBeTruthy();
+ });
+
+ test('应该有导航项', async ({ homePage }) => {
+ const navItems = await homePage.getNavigationItemCount();
+ expect(navItems).toBeGreaterThan(0);
+ });
+
+ test('应该能够点击导航项', async ({ homePage }) => {
+ const labels = await homePage.getAllNavigationLabels();
+ if (labels.length > 0) {
+ await homePage.clickNavigationItem(labels[0]);
+ await homePage.page.waitForTimeout(1000);
+ }
+ });
+
+ test('应该显示立即咨询按钮', async ({ homePage }) => {
+ const contactButton = homePage.page.locator('a:has-text("立即咨询")').first();
+ await expect(contactButton).toBeVisible();
+ });
+
+ test('应该能够点击立即咨询按钮', async ({ homePage }) => {
+ const contactButton = homePage.page.locator('a:has-text("立即咨询")').first();
+ await contactButton.click();
+ await homePage.page.waitForTimeout(2000);
+ const url = homePage.page.url();
+ console.log('点击立即咨询后的URL:', url);
+ expect(url).toContain('/contact');
+ });
+
+ test('应该显示移动端菜单按钮', async ({ homePage }) => {
+ await expect(homePage.mobileMenuButton).toBeVisible();
+ });
+
+ test('应该能够打开移动端菜单', async ({ homePage }) => {
+ await homePage.openMobileMenu();
+ await expect(homePage.mobileMenu).toBeVisible();
+ });
+
+ test('应该能够关闭移动端菜单', async ({ homePage }) => {
+ await homePage.openMobileMenu();
+ await homePage.closeMobileMenu();
+ await expect(homePage.mobileMenu).not.toBeVisible();
+ });
+
+ test('应该有正确的导航标签', async ({ homePage }) => {
+ const labels = await homePage.getAllNavigationLabels();
+ expect(labels.length).toBeGreaterThan(0);
+ labels.forEach(label => {
+ expect(label).toBeTruthy();
+ expect(label.length).toBeGreaterThan(0);
+ });
+ });
+
+ test('应该能够滚动到各个区块', async ({ homePage }) => {
+ const sections = ['services', 'products', 'cases', 'about', 'news', 'contact'];
+ for (const sectionId of sections) {
+ await homePage.scrollToSection(sectionId);
+ const isVisible = await homePage.isSectionVisible(sectionId);
+ expect(isVisible).toBe(true);
+ }
+ });
+
+ test('应该能够滚动到页面顶部', async ({ homePage }) => {
+ await homePage.scrollToBottom();
+ await homePage.scrollToTop();
+ const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
+ expect(scrollPosition).toBe(0);
+ });
+
+ test('应该能够滚动到页面底部', async ({ homePage }) => {
+ await homePage.scrollToBottom();
+ const scrollPosition = await homePage.page.evaluate(() => window.scrollY);
+ expect(scrollPosition).toBeGreaterThan(0);
+ });
+
+ test('应该显示所有区块', async ({ homePage }) => {
+ const sectionIds = await homePage.getAllSectionIds();
+ expect(sectionIds.length).toBeGreaterThan(0);
+ sectionIds.forEach(sectionId => {
+ expect(sectionId).toBeTruthy();
+ });
+ });
+
+ test('应该能够通过导航跳转到区块', async ({ homePage }) => {
+ const labels = await homePage.getAllNavigationLabels();
+ if (labels.length > 1) {
+ await homePage.clickNavigationItem(labels[1]);
+ await homePage.page.waitForTimeout(1000);
+ const url = homePage.page.url();
+ expect(url).toContain('#');
+ }
+ });
+
+ test('应该显示页脚', async ({ homePage }) => {
+ await homePage.scrollToBottom();
+ await expect(homePage.footer).toBeVisible();
+ });
+
+ test('应该有正确的页面标题', async ({ homePage }) => {
+ const title = await homePage.getTitle();
+ expect(title).toBeTruthy();
+ expect(title.length).toBeGreaterThan(0);
+ });
+
+ test('应该没有控制台错误', async ({ homePage, page }) => {
+ const errors: string[] = [];
+ page.on('console', msg => {
+ if (msg.type() === 'error') {
+ errors.push(msg.text());
+ }
+ });
+ await homePage.waitForPageLoad();
+ expect(errors.length).toBe(0);
+ });
+});
diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts b/e2e/src/tests/visual/contact-page.visual.spec.ts
new file mode 100644
index 0000000..365a063
--- /dev/null
+++ b/e2e/src/tests/visual/contact-page.visual.spec.ts
@@ -0,0 +1,298 @@
+import { test, expect } from '../../fixtures/base.fixture';
+
+test.describe('联系页面视觉回归测试 @visual', () => {
+ test('联系页面 - 页面头部应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.pageHeader).toHaveScreenshot('contact-page-header.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('联系页面 - 联系表单应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.contactForm).toHaveScreenshot('contact-form.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('联系页面 - 联系信息卡片应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.contactInfoCard).toHaveScreenshot('contact-info-card.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('联系页面 - 工作时间卡片应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.workHoursCard).toHaveScreenshot('work-hours-card.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('联系页面 - 表单字段应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.nameInput).toHaveScreenshot('name-input.png', {
+ maxDiffPixels: 20,
+ threshold: 0.1,
+ });
+ });
+
+ test('联系页面 - 邮箱字段应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.emailInput).toHaveScreenshot('email-input.png', {
+ maxDiffPixels: 20,
+ threshold: 0.1,
+ });
+ });
+
+ test('联系页面 - 主题字段应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.subjectInput).toHaveScreenshot('subject-input.png', {
+ maxDiffPixels: 20,
+ threshold: 0.1,
+ });
+ });
+
+ test('联系页面 - 消息字段应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.messageInput).toHaveScreenshot('message-input.png', {
+ maxDiffPixels: 50,
+ threshold: 0.15,
+ });
+ });
+
+ test('联系页面 - 提交按钮应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.submitButton).toHaveScreenshot('submit-button.png', {
+ maxDiffPixels: 30,
+ threshold: 0.15,
+ });
+ });
+
+ test('联系页面 - 完整页面应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(page).toHaveScreenshot('contact-full-page.png', {
+ fullPage: true,
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('联系页面 - 滚动后页面应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+ await contactPage.scrollToForm();
+ await contactPage.page.waitForTimeout(500);
+
+ await expect(page).toHaveScreenshot('contact-scrolled-page.png', {
+ fullPage: true,
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('联系页面 - 移动端视图应该与基线匹配', async ({ contactPage, page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(page).toHaveScreenshot('contact-mobile-view.png', {
+ fullPage: true,
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('联系页面 - 平板端视图应该与基线匹配', async ({ contactPage, page }) => {
+ await page.setViewportSize({ width: 768, height: 1024 });
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(page).toHaveScreenshot('contact-tablet-view.png', {
+ fullPage: true,
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('联系页面 - 桌面端视图应该与基线匹配', async ({ contactPage, page }) => {
+ await page.setViewportSize({ width: 1280, height: 720 });
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(page).toHaveScreenshot('contact-desktop-view.png', {
+ fullPage: true,
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('联系页面 - 填写表单后应该与基线匹配', async ({ contactPage, page, testDataGenerator }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillContactForm(formData);
+ await contactPage.page.waitForTimeout(500);
+
+ await expect(contactPage.contactForm).toHaveScreenshot('contact-form-filled.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('联系页面 - 提交成功后应该与基线匹配', async ({ contactPage, page, testDataGenerator }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const formData = testDataGenerator.generateContactFormData();
+ await contactPage.fillAndSubmitForm(formData);
+ await contactPage.waitForFormSubmission();
+
+ await expect(contactPage.successMessage).toHaveScreenshot('contact-success.png', {
+ maxDiffPixels: 50,
+ threshold: 0.15,
+ });
+ });
+
+ test('联系页面 - 应该能够捕获视觉差异', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ path: 'test-results/screenshots/contact-page-visual.png',
+ });
+
+ expect(screenshot).toBeTruthy();
+ });
+
+ test('联系页面 - 应该能够比较视觉差异', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.contactForm).toHaveScreenshot('contact-comparison.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ animations: 'disabled',
+ });
+ });
+
+ test('联系页面 - 应该能够禁用动画进行截图', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(page).toHaveScreenshot('contact-no-animations.png', {
+ fullPage: true,
+ animations: 'disabled',
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('联系页面 - 应该能够捕获高DPI截图', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ scale: 'device',
+ path: 'test-results/screenshots/contact-high-dpi.png',
+ });
+
+ expect(screenshot).toBeTruthy();
+ });
+
+ test('联系页面 - 表单字段焦点状态应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await contactPage.nameInput.focus();
+ await contactPage.page.waitForTimeout(300);
+
+ await expect(contactPage.nameInput).toHaveScreenshot('name-input-focused.png', {
+ maxDiffPixels: 30,
+ threshold: 0.15,
+ });
+ });
+
+ test('联系页面 - 表单字段悬停状态应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await contactPage.nameInput.hover();
+ await contactPage.page.waitForTimeout(300);
+
+ await expect(contactPage.nameInput).toHaveScreenshot('name-input-hover.png', {
+ maxDiffPixels: 30,
+ threshold: 0.15,
+ });
+ });
+
+ test('联系页面 - 提交按钮悬停状态应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await contactPage.submitButton.hover();
+ await contactPage.page.waitForTimeout(300);
+
+ await expect(contactPage.submitButton).toHaveScreenshot('submit-button-hover.png', {
+ maxDiffPixels: 30,
+ threshold: 0.15,
+ });
+ });
+
+ test('联系页面 - 所有表单字段应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.contactForm).toHaveScreenshot('all-form-fields.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('联系页面 - 联系信息应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.contactInfoCard).toHaveScreenshot('contact-info.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('联系页面 - 工作时间应该与基线匹配', async ({ contactPage, page }) => {
+ await contactPage.goto();
+ await contactPage.waitForPageLoad();
+
+ await expect(contactPage.workHoursCard).toHaveScreenshot('work-hours.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+});
diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts b/e2e/src/tests/visual/home-page.visual.spec.ts
new file mode 100644
index 0000000..6b45d31
--- /dev/null
+++ b/e2e/src/tests/visual/home-page.visual.spec.ts
@@ -0,0 +1,298 @@
+import { test, expect } from '../../fixtures/base.fixture';
+
+test.describe('首页视觉回归测试 @visual', () => {
+ test('首页 - Hero区块应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.heroSection).toHaveScreenshot('hero-section.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('首页 - Services区块应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToSection('services');
+ await homePage.page.waitForTimeout(500);
+
+ await expect(homePage.servicesSection).toHaveScreenshot('services-section.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('首页 - Products区块应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToSection('products');
+ await homePage.page.waitForTimeout(500);
+
+ await expect(homePage.productsSection).toHaveScreenshot('products-section.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('首页 - Cases区块应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToSection('cases');
+ await homePage.page.waitForTimeout(500);
+
+ await expect(homePage.casesSection).toHaveScreenshot('cases-section.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('首页 - About区块应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToSection('about');
+ await homePage.page.waitForTimeout(500);
+
+ await expect(homePage.aboutSection).toHaveScreenshot('about-section.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('首页 - News区块应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToSection('news');
+ await homePage.page.waitForTimeout(500);
+
+ await expect(homePage.newsSection).toHaveScreenshot('news-section.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('首页 - Contact区块应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToSection('contact');
+ await homePage.page.waitForTimeout(500);
+
+ await expect(homePage.contactSection).toHaveScreenshot('contact-section.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('首页 - Header应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.header).toHaveScreenshot('header.png', {
+ maxDiffPixels: 50,
+ threshold: 0.1,
+ });
+ });
+
+ test('首页 - Footer应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToBottom();
+ await homePage.page.waitForTimeout(500);
+
+ await expect(homePage.footer).toHaveScreenshot('footer.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('首页 - Logo应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.logo).toHaveScreenshot('logo.png', {
+ maxDiffPixels: 10,
+ threshold: 0.05,
+ });
+ });
+
+ test('首页 - 导航应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.navigation).toHaveScreenshot('navigation.png', {
+ maxDiffPixels: 50,
+ threshold: 0.1,
+ });
+ });
+
+ test('首页 - 完整页面应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(page).toHaveScreenshot('full-page.png', {
+ fullPage: true,
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('首页 - 滚动后页面应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToSection('services');
+ await homePage.page.waitForTimeout(500);
+
+ await expect(page).toHaveScreenshot('scrolled-page.png', {
+ fullPage: true,
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('首页 - Hero区块标题应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const title = homePage.heroSection.locator('h1, h2').first();
+ await expect(title).toHaveScreenshot('hero-title.png', {
+ maxDiffPixels: 20,
+ threshold: 0.1,
+ });
+ });
+
+ test('首页 - 悬停状态应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const firstNavItem = homePage.navigation.locator('a').first();
+ await firstNavItem.hover();
+ await homePage.page.waitForTimeout(300);
+
+ await expect(homePage.navigation).toHaveScreenshot('nav-hover.png', {
+ maxDiffPixels: 50,
+ threshold: 0.1,
+ });
+ });
+
+ test('首页 - 移动端视图应该与基线匹配', async ({ homePage, page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(page).toHaveScreenshot('mobile-view.png', {
+ fullPage: true,
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('首页 - 平板端视图应该与基线匹配', async ({ homePage, page }) => {
+ await page.setViewportSize({ width: 768, height: 1024 });
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(page).toHaveScreenshot('tablet-view.png', {
+ fullPage: true,
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('首页 - 桌面端视图应该与基线匹配', async ({ homePage, page }) => {
+ await page.setViewportSize({ width: 1280, height: 720 });
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(page).toHaveScreenshot('desktop-view.png', {
+ fullPage: true,
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('首页 - 移动端菜单应该与基线匹配', async ({ homePage, page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.openMobileMenu();
+ await homePage.page.waitForTimeout(300);
+
+ await expect(homePage.mobileMenu).toHaveScreenshot('mobile-menu.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ });
+ });
+
+ test('首页 - 滚动后Header应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToSection('services');
+ await homePage.page.waitForTimeout(500);
+
+ await expect(homePage.header).toHaveScreenshot('header-scrolled.png', {
+ maxDiffPixels: 50,
+ threshold: 0.1,
+ });
+ });
+
+ test('首页 - 所有区块组合应该与基线匹配', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+ await homePage.scrollToBottom();
+ await homePage.page.waitForTimeout(500);
+
+ await expect(page).toHaveScreenshot('all-sections.png', {
+ fullPage: true,
+ maxDiffPixels: 300,
+ threshold: 0.4,
+ });
+ });
+
+ test('首页 - 应该能够捕获视觉差异', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ path: 'test-results/screenshots/homepage-visual.png',
+ });
+
+ expect(screenshot).toBeTruthy();
+ });
+
+ test('首页 - 应该能够比较视觉差异', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(homePage.heroSection).toHaveScreenshot('hero-comparison.png', {
+ maxDiffPixels: 100,
+ threshold: 0.2,
+ animations: 'disabled',
+ });
+ });
+
+ test('首页 - 应该能够禁用动画进行截图', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ await expect(page).toHaveScreenshot('no-animations.png', {
+ fullPage: true,
+ animations: 'disabled',
+ maxDiffPixels: 200,
+ threshold: 0.3,
+ });
+ });
+
+ test('首页 - 应该能够捕获高DPI截图', async ({ homePage, page }) => {
+ await homePage.goto();
+ await homePage.waitForPageLoad();
+
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ scale: 'device',
+ path: 'test-results/screenshots/homepage-high-dpi.png',
+ });
+
+ expect(screenshot).toBeTruthy();
+ });
+});
diff --git a/e2e/src/types/index.ts b/e2e/src/types/index.ts
new file mode 100644
index 0000000..f3a1953
--- /dev/null
+++ b/e2e/src/types/index.ts
@@ -0,0 +1,117 @@
+export interface TestData {
+ name: string;
+ email: string;
+ phone?: string;
+ company?: string;
+ message?: string;
+}
+
+export interface ContactFormData extends TestData {
+ subject?: string;
+}
+
+export interface NavigationItem {
+ label: string;
+ href: string;
+ expectedUrl?: string;
+}
+
+export interface PerformanceMetrics {
+ loadTime: number;
+ firstContentfulPaint: number;
+ largestContentfulPaint: number;
+ timeToInteractive: number;
+ cumulativeLayoutShift: number;
+ firstInputDelay: number;
+}
+
+export interface PerformanceThresholds {
+ loadTime: number;
+ firstContentfulPaint: number;
+ largestContentfulPaint: number;
+ timeToInteractive: number;
+ cumulativeLayoutShift: number;
+ firstInputDelay: number;
+}
+
+export interface DeviceConfig {
+ name: string;
+ viewport: { width: number; height: number };
+ userAgent?: string;
+ isMobile: boolean;
+}
+
+export interface TestResult {
+ test: string;
+ status: 'passed' | 'failed' | 'skipped';
+ duration: number;
+ error?: string;
+}
+
+export interface AccessibilityViolation {
+ id: string;
+ impact: 'critical' | 'serious' | 'moderate' | 'minor';
+ description: string;
+ help: string;
+ helpUrl: string;
+ nodes: any[];
+}
+
+export interface VisualRegressionResult {
+ baseline: string;
+ current: string;
+ diff?: string;
+ mismatchedPixels: number;
+ passed: boolean;
+}
+
+export interface PageElement {
+ selector: string;
+ text?: string;
+ visible?: boolean;
+ enabled?: boolean;
+}
+
+export interface TestConfig {
+ baseURL: string;
+ timeout: number;
+ retries: number;
+ headless: boolean;
+ screenshotOnFailure: boolean;
+ traceOnFailure: boolean;
+ videoOnFailure: boolean;
+}
+
+export interface TestCase {
+ name: string;
+ description: string;
+ tags: string[];
+ priority: 'critical' | 'high' | 'medium' | 'low';
+ dependencies?: string[];
+}
+
+export interface TestSuite {
+ name: string;
+ description: string;
+ tests: TestCase[];
+}
+
+export interface ApiResponse {
+ status: number;
+ data: any;
+ message?: string;
+}
+
+export interface ErrorDetails {
+ message: string;
+ stack?: string;
+ code?: string;
+}
+
+export interface TestEnvironment {
+ name: string;
+ url: string;
+ isProduction: boolean;
+ browser: string;
+ version: string;
+}
diff --git a/e2e/src/utils/PerformanceMonitor.ts b/e2e/src/utils/PerformanceMonitor.ts
new file mode 100644
index 0000000..f9c22fd
--- /dev/null
+++ b/e2e/src/utils/PerformanceMonitor.ts
@@ -0,0 +1,313 @@
+import { Page } from '@playwright/test';
+import { PerformanceMetrics, PerformanceThresholds } from '../types';
+
+export class PerformanceMonitor {
+ private page: Page;
+ private metrics: PerformanceMetrics;
+ private startTime: number;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.metrics = {
+ loadTime: 0,
+ firstContentfulPaint: 0,
+ largestContentfulPaint: 0,
+ timeToInteractive: 0,
+ cumulativeLayoutShift: 0,
+ firstInputDelay: 0,
+ };
+ this.startTime = 0;
+ }
+
+ async startMonitoring(): Promise {
+ this.startTime = Date.now();
+
+ await this.page.evaluate(() => {
+ window.performance.clearResourceTimings();
+ });
+
+ await this.page.evaluate(() => {
+ if ('PerformanceObserver' in window) {
+ const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ entries.forEach((entry) => {
+ if (entry.entryType === 'layout-shift' && !(entry as any).hadRecentInput) {
+ (window as any).cumulativeLayoutShift = ((window as any).cumulativeLayoutShift || 0) + (entry as any).value;
+ }
+ });
+ });
+ observer.observe({ entryTypes: ['layout-shift'] });
+ }
+ });
+ }
+
+ async collectMetrics(): Promise {
+ const navigationTiming = await this.page.evaluate(() => {
+ const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
+ return {
+ loadTime: timing.loadEventEnd - timing.fetchStart,
+ domContentLoaded: timing.domContentLoadedEventEnd - timing.fetchStart,
+ firstPaint: timing.responseStart - timing.fetchStart,
+ };
+ });
+
+ const paintTiming = await this.page.evaluate(() => {
+ const paints = performance.getEntriesByType('paint');
+ const fcp = paints.find((p) => p.name === 'first-contentful-paint');
+ return {
+ firstContentfulPaint: fcp ? fcp.startTime : 0,
+ };
+ });
+
+ const lcp = await this.page.evaluate(() => {
+ return new Promise((resolve) => {
+ if ('PerformanceObserver' in window) {
+ const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ const lastEntry = entries[entries.length - 1];
+ resolve(lastEntry ? lastEntry.startTime : 0);
+ });
+ observer.observe({ entryTypes: ['largest-contentful-paint'] });
+ } else {
+ resolve(0);
+ }
+ });
+ });
+
+ const cls = await this.page.evaluate(() => {
+ return (window as any).cumulativeLayoutShift || 0;
+ });
+
+ const tti = await this.page.evaluate(() => {
+ return new Promise((resolve) => {
+ if ('PerformanceObserver' in window) {
+ const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ const longTasks = entries.filter((e) => e.duration > 50);
+ if (longTasks.length > 0) {
+ resolve(longTasks[0].startTime);
+ }
+ });
+ observer.observe({ entryTypes: ['longtask'] });
+ } else {
+ resolve(0);
+ }
+ });
+ });
+
+ const fid = await this.page.evaluate(() => {
+ return new Promise((resolve) => {
+ if ('PerformanceObserver' in window) {
+ const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ if (entries.length > 0) {
+ resolve(entries[0].processingStart - entries[0].startTime);
+ }
+ });
+ observer.observe({ entryTypes: ['first-input'] });
+ } else {
+ resolve(0);
+ }
+ });
+ });
+
+ this.metrics = {
+ loadTime: navigationTiming.loadTime,
+ firstContentfulPaint: paintTiming.firstContentfulPaint,
+ largestContentfulPaint: lcp,
+ timeToInteractive: tti,
+ cumulativeLayoutShift: cls,
+ firstInputDelay: fid,
+ };
+
+ return this.metrics;
+ }
+
+ async measurePageLoad(): Promise {
+ const startTime = Date.now();
+ await this.page.waitForLoadState('networkidle');
+ const endTime = Date.now();
+ return endTime - startTime;
+ }
+
+ async measureFirstContentfulPaint(): Promise {
+ const fcp = await this.page.evaluate(() => {
+ const paints = performance.getEntriesByType('paint');
+ const fcpEntry = paints.find((p) => p.name === 'first-contentful-paint');
+ return fcpEntry ? fcpEntry.startTime : 0;
+ });
+ return fcp;
+ }
+
+ async measureLargestContentfulPaint(): Promise {
+ const lcp = await this.page.evaluate(() => {
+ return new Promise((resolve) => {
+ if ('PerformanceObserver' in window) {
+ const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ const lastEntry = entries[entries.length - 1];
+ resolve(lastEntry ? lastEntry.startTime : 0);
+ });
+ observer.observe({ entryTypes: ['largest-contentful-paint'] });
+ setTimeout(() => resolve(0), 5000);
+ } else {
+ resolve(0);
+ }
+ });
+ });
+ return lcp;
+ }
+
+ async measureCumulativeLayoutShift(): Promise {
+ const cls = await this.page.evaluate(() => {
+ return (window as any).cumulativeLayoutShift || 0;
+ });
+ return cls;
+ }
+
+ async measureTimeToInteractive(): Promise {
+ const tti = await this.page.evaluate(() => {
+ return new Promise((resolve) => {
+ if ('PerformanceObserver' in window) {
+ const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ const longTasks = entries.filter((e) => e.duration > 50);
+ if (longTasks.length > 0) {
+ resolve(longTasks[0].startTime);
+ }
+ });
+ observer.observe({ entryTypes: ['longtask'] });
+ setTimeout(() => resolve(0), 10000);
+ } else {
+ resolve(0);
+ }
+ });
+ });
+ return tti;
+ }
+
+ async measureFirstInputDelay(): Promise {
+ const fid = await this.page.evaluate(() => {
+ return new Promise((resolve) => {
+ if ('PerformanceObserver' in window) {
+ const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ if (entries.length > 0) {
+ resolve(entries[0].processingStart - entries[0].startTime);
+ }
+ });
+ observer.observe({ entryTypes: ['first-input'] });
+ setTimeout(() => resolve(0), 5000);
+ } else {
+ resolve(0);
+ }
+ });
+ });
+ return fid;
+ }
+
+ async measureResourceTiming(): Promise {
+ const resources = await this.page.evaluate(() => {
+ return performance.getEntriesByType('resource').map((r) => ({
+ name: r.name,
+ duration: r.duration,
+ size: (r as any).transferSize,
+ type: r.initiatorType,
+ }));
+ });
+ return resources;
+ }
+
+ async measureMemoryUsage(): Promise {
+ const memory = await this.page.evaluate(() => {
+ return (performance as any).memory?.usedJSHeapSize || 0;
+ });
+ return memory;
+ }
+
+ async measureFrameRate(): Promise {
+ const frameRate = await this.page.evaluate(() => {
+ return new Promise((resolve) => {
+ let frames = 0;
+ const startTime = performance.now();
+
+ function countFrames() {
+ frames++;
+ if (performance.now() - startTime >= 1000) {
+ resolve(frames);
+ } else {
+ requestAnimationFrame(countFrames);
+ }
+ }
+
+ requestAnimationFrame(countFrames);
+ });
+ });
+ return frameRate;
+ }
+
+ async measureDomContentLoaded(): Promise {
+ const dcl = await this.page.evaluate(() => {
+ const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
+ return timing.domContentLoadedEventEnd - timing.fetchStart;
+ });
+ return dcl;
+ }
+
+ validateMetrics(thresholds: PerformanceThresholds): { passed: boolean; violations: string[] } {
+ const violations: string[] = [];
+
+ if (this.metrics.loadTime > thresholds.loadTime) {
+ violations.push(`页面加载时间 ${this.metrics.loadTime}ms 超过阈值 ${thresholds.loadTime}ms`);
+ }
+ if (this.metrics.firstContentfulPaint > thresholds.firstContentfulPaint) {
+ violations.push(`首次内容绘制 ${this.metrics.firstContentfulPaint}ms 超过阈值 ${thresholds.firstContentfulPaint}ms`);
+ }
+ if (this.metrics.largestContentfulPaint > thresholds.largestContentfulPaint) {
+ violations.push(`最大内容绘制 ${this.metrics.largestContentfulPaint}ms 超过阈值 ${thresholds.largestContentfulPaint}ms`);
+ }
+ if (this.metrics.timeToInteractive > thresholds.timeToInteractive) {
+ violations.push(`可交互时间 ${this.metrics.timeToInteractive}ms 超过阈值 ${thresholds.timeToInteractive}ms`);
+ }
+ if (this.metrics.cumulativeLayoutShift > thresholds.cumulativeLayoutShift) {
+ violations.push(`累积布局偏移 ${this.metrics.cumulativeLayoutShift} 超过阈值 ${thresholds.cumulativeLayoutShift}`);
+ }
+ if (this.metrics.firstInputDelay > thresholds.firstInputDelay) {
+ violations.push(`首次输入延迟 ${this.metrics.firstInputDelay}ms 超过阈值 ${thresholds.firstInputDelay}ms`);
+ }
+
+ return {
+ passed: violations.length === 0,
+ violations,
+ };
+ }
+
+ getMetrics(): PerformanceMetrics {
+ return this.metrics;
+ }
+
+ async generateReport(): Promise {
+ const metrics = await this.collectMetrics();
+ const resources = await this.measureResourceTiming();
+
+ let report = '=== 性能测试报告 ===\n\n';
+ report += '核心指标:\n';
+ report += `- 页面加载时间: ${metrics.loadTime.toFixed(2)}ms\n`;
+ report += `- 首次内容绘制: ${metrics.firstContentfulPaint.toFixed(2)}ms\n`;
+ report += `- 最大内容绘制: ${metrics.largestContentfulPaint.toFixed(2)}ms\n`;
+ report += `- 可交互时间: ${metrics.timeToInteractive.toFixed(2)}ms\n`;
+ report += `- 累积布局偏移: ${metrics.cumulativeLayoutShift.toFixed(4)}\n`;
+ report += `- 首次输入延迟: ${metrics.firstInputDelay.toFixed(2)}ms\n\n`;
+
+ report += '资源加载:\n';
+ const totalResources = resources.length;
+ const totalSize = resources.reduce((sum, r) => sum + (r.size || 0), 0);
+ const avgDuration = resources.reduce((sum, r) => sum + r.duration, 0) / totalResources;
+
+ report += `- 总资源数: ${totalResources}\n`;
+ report += `- 总大小: ${(totalSize / 1024).toFixed(2)}KB\n`;
+ report += `- 平均加载时间: ${avgDuration.toFixed(2)}ms\n`;
+
+ return report;
+ }
+}
diff --git a/e2e/src/utils/TestDataGenerator.ts b/e2e/src/utils/TestDataGenerator.ts
new file mode 100644
index 0000000..eb6a2b3
--- /dev/null
+++ b/e2e/src/utils/TestDataGenerator.ts
@@ -0,0 +1,254 @@
+import { ContactFormData, TestData } from '../types';
+
+export class TestDataGenerator {
+ private static readonly FIRST_NAMES = ['张', '李', '王', '刘', '陈', '杨', '赵', '黄', '周', '吴'];
+ private static readonly LAST_NAMES = ['伟', '芳', '娜', '秀英', '敏', '静', '丽', '强', '磊', '军'];
+ private static readonly COMPANIES = ['科技有限公司', '信息技术有限公司', '网络技术有限公司', '数据科技有限公司', '智能科技有限公司'];
+ private static readonly SUBJECTS = ['产品咨询', '技术支持', '商务合作', '其他', '意见反馈'];
+
+ static generateName(): string {
+ const first = this.FIRST_NAMES[Math.floor(Math.random() * this.FIRST_NAMES.length)];
+ const last = this.LAST_NAMES[Math.floor(Math.random() * this.LAST_NAMES.length)];
+ return `${first}${last}`;
+ }
+
+ static generateEmail(name?: string): string {
+ const username = name || this.generateName();
+ const domains = ['example.com', 'test.com', 'demo.com'];
+ const domain = domains[Math.floor(Math.random() * domains.length)];
+ return `${username}@${domain}`;
+ }
+
+ static generatePhone(): string {
+ const prefix = ['138', '139', '136', '137', '158', '159'][Math.floor(Math.random() * 6)];
+ const middle = Math.floor(Math.random() * 9000 + 1000);
+ const suffix = Math.floor(Math.random() * 9000 + 1000);
+ return `${prefix}${middle}${suffix}`;
+ }
+
+ static generateCompany(): string {
+ const prefix = ['创新', '未来', '智慧', '科技', '数字'][Math.floor(Math.random() * 5)];
+ const suffix = this.COMPANIES[Math.floor(Math.random() * this.COMPANIES.length)];
+ return `${prefix}${suffix}`;
+ }
+
+ static generateMessage(minLength: number = 10, maxLength: number = 100): string {
+ const messages = [
+ '您好,我对贵公司的产品很感兴趣,希望能了解更多信息。',
+ '请问贵公司是否有相关的技术支持服务?',
+ '我们正在寻找合作伙伴,希望能与贵公司建立联系。',
+ '贵公司的产品功能很强大,希望能安排一次演示。',
+ '我对贵公司的服务有些建议和想法,希望能与您交流。',
+ '请问贵公司的产品价格如何?是否有优惠政策?',
+ '我们公司正在评估相关技术方案,希望能了解贵公司的解决方案。',
+ '您好,我想咨询一下贵公司的产品定制服务。',
+ ];
+ return messages[Math.floor(Math.random() * messages.length)];
+ }
+
+ static generateSubject(): string {
+ return this.SUBJECTS[Math.floor(Math.random() * this.SUBJECTS.length)];
+ }
+
+ static generateContactFormData(): ContactFormData {
+ return {
+ name: this.generateName(),
+ email: this.generateEmail(),
+ phone: this.generatePhone(),
+ company: this.generateCompany(),
+ message: this.generateMessage(),
+ subject: this.generateSubject(),
+ };
+ }
+
+ static generateTestData(): TestData {
+ return {
+ name: this.generateName(),
+ email: this.generateEmail(),
+ phone: this.generatePhone(),
+ company: this.generateCompany(),
+ message: this.generateMessage(),
+ };
+ }
+
+ static generateInvalidEmail(): string {
+ const invalidEmails = [
+ 'invalid-email',
+ '@example.com',
+ 'user@',
+ 'user@domain',
+ 'user domain.com',
+ ];
+ return invalidEmails[Math.floor(Math.random() * invalidEmails.length)];
+ }
+
+ static generateInvalidPhone(): string {
+ const invalidPhones = [
+ '123',
+ '1234567890123',
+ 'abcdefghijk',
+ '123-456-7890',
+ ];
+ return invalidPhones[Math.floor(Math.random() * invalidPhones.length)];
+ }
+
+ static generateShortMessage(): string {
+ return '测试';
+ }
+
+ static generateLongMessage(): string {
+ return 'A'.repeat(1001);
+ }
+
+ static generateSpecialCharacters(): string {
+ return '!@#$%^&*()_+-=[]{}|;:,.<>?/~`';
+ }
+
+ static generateChineseCharacters(): string {
+ return '这是一段中文测试文本,包含了一些特殊字符:!@#¥%……&*()——+';
+ }
+
+ static generateMixedContent(): string {
+ return 'Hello 世界!This is a mixed content test 测试。';
+ }
+
+ static generateNumberString(length: number): string {
+ return Math.random().toString().slice(2, 2 + length);
+ }
+
+ static generateAlphanumeric(length: number): string {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let result = '';
+ for (let i = 0; i < length; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return result;
+ }
+
+ static generateWhitespace(): string {
+ return ' \t\n\r ';
+ }
+
+ static generateUrl(): string {
+ const urls = [
+ 'https://example.com',
+ 'http://test.com',
+ 'https://demo.com/path',
+ 'http://example.com/page?param=value',
+ ];
+ return urls[Math.floor(Math.random() * urls.length)];
+ }
+
+ static generateDate(): string {
+ const date = new Date();
+ date.setDate(date.getDate() + Math.floor(Math.random() * 30));
+ return date.toISOString().split('T')[0];
+ }
+
+ static generateTime(): string {
+ const hours = Math.floor(Math.random() * 24);
+ const minutes = Math.floor(Math.random() * 60);
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
+ }
+
+ static generateBoolean(): boolean {
+ return Math.random() < 0.5;
+ }
+
+ static generateNumber(min: number = 0, max: number = 100): number {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+ }
+
+ static generateFloat(min: number = 0, max: number = 100, decimals: number = 2): number {
+ const num = Math.random() * (max - min) + min;
+ return parseFloat(num.toFixed(decimals));
+ }
+
+ static generateArray(generator: () => T, length: number = 5): T[] {
+ return Array.from({ length }, generator);
+ }
+
+ static generateObject(): Record {
+ return {
+ name: this.generateName(),
+ email: this.generateEmail(),
+ age: this.generateNumber(18, 65),
+ active: this.generateBoolean(),
+ score: this.generateFloat(0, 100),
+ };
+ }
+
+ static generateUuid(): string {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+ const r = Math.random() * 16 | 0;
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+ }
+
+ static generateIPv4(): string {
+ return `${this.generateNumber(0, 255)}.${this.generateNumber(0, 255)}.${this.generateNumber(0, 255)}.${this.generateNumber(0, 255)}`;
+ }
+
+ static generateMacAddress(): string {
+ const hex = '0123456789ABCDEF';
+ let mac = '';
+ for (let i = 0; i < 6; i++) {
+ if (i > 0) mac += ':';
+ mac += hex[Math.floor(Math.random() * 16)];
+ mac += hex[Math.floor(Math.random() * 16)];
+ }
+ return mac;
+ }
+
+ static generateColor(): string {
+ return `#${this.generateAlphanumeric(6)}`;
+ }
+
+ static generateJson(): string {
+ const obj = this.generateObject();
+ return JSON.stringify(obj);
+ }
+
+ static generateXml(): string {
+ return `
+
+ ${this.generateName()}
+ ${this.generateEmail()}
+ ${this.generateNumber(18, 65)}
+`;
+ }
+
+ static generateCsv(): string {
+ const headers = ['Name,Email,Phone,Company'];
+ const rows = this.generateArray(() =>
+ `${this.generateName()},${this.generateEmail()},${this.generatePhone()},${this.generateCompany()}`,
+ 5
+ );
+ return [...headers, ...rows].join('\n');
+ }
+
+ static generateHtml(): string {
+ return `
+
+
+ Test Page
+
+
+ ${this.generateName()}
+ ${this.generateMessage()}
+
+`;
+ }
+
+ static generateMarkdown(): string {
+ return `# ${this.generateName()}
+
+## Contact Information
+- Email: ${this.generateEmail()}
+- Phone: ${this.generatePhone()}
+
+## Message
+${this.generateMessage()}`;
+ }
+}
diff --git a/e2e/src/utils/devices.ts b/e2e/src/utils/devices.ts
new file mode 100644
index 0000000..3ab3833
--- /dev/null
+++ b/e2e/src/utils/devices.ts
@@ -0,0 +1,122 @@
+import { DeviceConfig } from '../types';
+
+export const devices: Record = {
+ 'desktop-1920x1080': {
+ name: 'Desktop 1920x1080',
+ viewport: { width: 1920, height: 1080 },
+ isMobile: false,
+ },
+ 'desktop-1366x768': {
+ name: 'Desktop 1366x768',
+ viewport: { width: 1366, height: 768 },
+ isMobile: false,
+ },
+ 'desktop-1280x720': {
+ name: 'Desktop 1280x720',
+ viewport: { width: 1280, height: 720 },
+ isMobile: false,
+ },
+ 'laptop-1440x900': {
+ name: 'Laptop 1440x900',
+ viewport: { width: 1440, height: 900 },
+ isMobile: false,
+ },
+ 'laptop-1024x768': {
+ name: 'Laptop 1024x768',
+ viewport: { width: 1024, height: 768 },
+ isMobile: false,
+ },
+ 'tablet-768x1024': {
+ name: 'Tablet 768x1024',
+ viewport: { width: 768, height: 1024 },
+ isMobile: true,
+ },
+ 'tablet-834x1194': {
+ name: 'Tablet 834x1194 (iPad Pro)',
+ viewport: { width: 834, height: 1194 },
+ isMobile: true,
+ },
+ 'mobile-375x667': {
+ name: 'Mobile 375x667 (iPhone SE)',
+ viewport: { width: 375, height: 667 },
+ isMobile: true,
+ },
+ 'mobile-390x844': {
+ name: 'Mobile 390x844 (iPhone 12)',
+ viewport: { width: 390, height: 844 },
+ isMobile: true,
+ },
+ 'mobile-414x896': {
+ name: 'Mobile 414x896 (iPhone 11)',
+ viewport: { width: 414, height: 896 },
+ isMobile: true,
+ },
+ 'mobile-360x640': {
+ name: 'Mobile 360x640 (Android)',
+ viewport: { width: 360, height: 640 },
+ isMobile: true,
+ },
+ 'mobile-412x915': {
+ name: 'Mobile 412x915 (Pixel 5)',
+ viewport: { width: 412, height: 915 },
+ isMobile: true,
+ },
+};
+
+export const desktopDevices = Object.entries(devices)
+ .filter(([_, config]) => !config.isMobile)
+ .map(([key, config]) => ({ key, ...config }));
+
+export const mobileDevices = Object.entries(devices)
+ .filter(([_, config]) => config.isMobile)
+ .map(([key, config]) => ({ key, ...config }));
+
+export const tabletDevices = Object.entries(devices)
+ .filter(([_, config]) => config.isMobile && config.viewport.width >= 768)
+ .map(([key, config]) => ({ key, ...config }));
+
+export const getDevice = (key: string): DeviceConfig => {
+ return devices[key] || devices['desktop-1280x720'];
+};
+
+export const getAllDevices = (): DeviceConfig[] => {
+ return Object.values(devices);
+};
+
+export const getDesktopDevices = (): DeviceConfig[] => {
+ return desktopDevices.map(d => devices[d.key]);
+};
+
+export const getMobileDevices = (): DeviceConfig[] => {
+ return mobileDevices.map(d => devices[d.key]);
+};
+
+export const getTabletDevices = (): DeviceConfig[] => {
+ return tabletDevices.map(d => devices[d.key]);
+};
+
+export const getBreakpoints = () => {
+ return {
+ xs: 0,
+ sm: 640,
+ md: 768,
+ lg: 1024,
+ xl: 1280,
+ '2xl': 1536,
+ };
+};
+
+export const isMobile = (width: number): boolean => {
+ const breakpoints = getBreakpoints();
+ return width < breakpoints.lg;
+};
+
+export const isTablet = (width: number): boolean => {
+ const breakpoints = getBreakpoints();
+ return width >= breakpoints.md && width < breakpoints.lg;
+};
+
+export const isDesktop = (width: number): boolean => {
+ const breakpoints = getBreakpoints();
+ return width >= breakpoints.lg;
+};
diff --git a/e2e/test-report.md b/e2e/test-report.md
new file mode 100644
index 0000000..a8b1051
--- /dev/null
+++ b/e2e/test-report.md
@@ -0,0 +1,173 @@
+# E2E测试执行报告
+
+## 测试执行时间
+2026-02-27
+
+## 测试环境
+- 浏览器:Chromium
+- 基础URL:http://localhost:3001
+- 测试框架:Playwright + TypeScript
+
+## 测试结果汇总
+
+### 总体统计
+- 总测试数:166
+- 通过:69 (41.6%)
+- 失败:97 (58.4%)
+- 通过率:41.6%
+
+### 详细分类结果
+
+#### 1. 冒烟测试 (Smoke Tests)
+- 状态:✅ 基本完成
+- 通过:25
+- 失败:0
+- 通过率:100%
+
+**测试覆盖:**
+- 首页冒烟测试:✅ 通过
+- 联系页面冒烟测试:✅ 通过
+- 导航冒烟测试:✅ 通过
+
+**修复的问题:**
+- 修复了Logo选择器(从"Novalon"改为"四川睿新致远")
+- 修复了移动端菜单按钮选择器
+- 修复了联系页面卡片选择器
+- 修复了联系信息选择器(地址、电话、邮箱)
+- 修复了页面徽章选择器
+- 修复了BASE_URL配置(从3000改为3001)
+
+#### 2. 回归测试 (Regression Tests)
+- 状态:⚠️ 部分通过
+- 通过:25
+- 失败:23
+- 通过率:52.1%
+
+**失败原因分析:**
+- 联系表单提交功能未完全实现
+- 页面滚动事件处理需要优化
+- 移动端菜单交互需要完善
+- 浏览器导航功能需要调整
+
+#### 3. 性能测试 (Performance Tests)
+- 状态:❌ 大部分失败
+- 通过:3
+- 失败:30
+- 通过率:9.1%
+
+**失败原因分析:**
+- 页面加载时间超出阈值
+- 交互响应时间需要优化
+- 滚动性能需要改善
+- 资源加载效率有待提升
+
+**性能指标:**
+- 首页加载时间:需要优化
+- 联系页面加载时间:需要优化
+- 最大内容绘制(LCP):需要优化
+- 可交互时间(TTI):需要优化
+- 帧率:需要优化
+
+#### 4. 响应式测试 (Responsive Tests)
+- 状态:❌ 大部分失败
+- 通过:16
+- 失败:44
+- 通过率:26.7%
+
+**失败原因分析:**
+- 移动端布局适配需要完善
+- 平板端显示需要调整
+- 桌面端大屏幕显示需要优化
+- 移动端交互需要改进
+
+## 主要发现和建议
+
+### 1. 优先级1:关键功能修复
+- ✅ 首页基本功能正常
+- ✅ 联系页面基本功能正常
+- ✅ 导航功能基本正常
+- ⚠️ 联系表单提交需要完善
+- ⚠️ 移动端菜单交互需要优化
+
+### 2. 优先级2:性能优化
+- ❌ 页面加载时间需要优化
+- ❌ 交互响应时间需要优化
+- ❌ 滚动性能需要改善
+- ❌ 资源加载需要优化
+
+### 3. 优先级3:响应式适配
+- ❌ 移动端布局需要完善
+- ❌ 平板端显示需要调整
+- ❌ 大屏幕显示需要优化
+
+## TDD改进建议
+
+### 立即行动项
+1. 修复联系表单提交功能
+2. 优化移动端菜单交互
+3. 改善页面加载性能
+4. 优化滚动性能
+
+### 短期改进项
+1. 完善响应式布局
+2. 优化资源加载
+3. 改善交互响应时间
+4. 优化移动端体验
+
+### 长期优化项
+1. 实施性能监控
+2. 建立性能基准
+3. 持续优化用户体验
+4. 建立自动化性能测试
+
+## 测试覆盖率
+
+### 页面覆盖
+- ✅ 首页:100%
+- ✅ 联系页面:100%
+- ⚠️ 其他页面:0%
+
+### 功能覆盖
+- ✅ 基本导航:100%
+- ✅ 表单输入:100%
+- ⚠️ 表单提交:50%
+- ❌ 复杂交互:20%
+
+### 设备覆盖
+- ✅ 桌面端:100%
+- ⚠️ 平板端:40%
+- ❌ 移动端:30%
+
+## 下一步计划
+
+1. **修复关键功能**
+ - 完善联系表单提交
+ - 优化移动端菜单
+ - 修复页面滚动问题
+
+2. **性能优化**
+ - 实施代码分割
+ - 优化图片加载
+ - 改善缓存策略
+ - 优化CSS和JS
+
+3. **响应式改进**
+ - 完善移动端布局
+ - 优化平板端显示
+ - 改善大屏幕体验
+
+4. **测试扩展**
+ - 添加更多页面测试
+ - 增加复杂交互测试
+ - 完善移动端测试
+ - 添加跨浏览器测试
+
+## 结论
+
+E2E测试框架已成功搭建并运行,测试结果显示:
+- 基本功能(冒烟测试)运行良好,通过率100%
+- 复杂功能(回归测试)需要进一步完善,通过率52.1%
+- 性能指标需要大幅优化,通过率9.1%
+- 响应式适配需要重点改进,通过率26.7%
+
+建议按照优先级逐步修复问题,并建立持续测试和优化流程。
diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json
new file mode 100644
index 0000000..b8c081b
--- /dev/null
+++ b/e2e/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "commonjs",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "moduleResolution": "node",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "types": ["node", "@playwright/test"],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"],
+ "@pages/*": ["./src/pages/*"],
+ "@fixtures/*": ["./src/fixtures/*"],
+ "@utils/*": ["./src/utils/*"],
+ "@types/*": ["./src/types/*"]
+ }
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "test-results", "playwright-report"]
+}
diff --git a/src/app/(marketing)/cases/[id]/client.tsx b/src/app/(marketing)/cases/[id]/client.tsx
index de77f5d..ea44c21 100644
--- a/src/app/(marketing)/cases/[id]/client.tsx
+++ b/src/app/(marketing)/cases/[id]/client.tsx
@@ -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(null);
+ const router = useRouter();
useEffect(() => {
const observer = new IntersectionObserver(
@@ -67,12 +68,14 @@ export function CaseDetailClient({ caseItem }: CaseDetailClientProps) {
-
-
-
+
{caseItem.industry}
diff --git a/src/app/(marketing)/services/[id]/client.tsx b/src/app/(marketing)/services/[id]/client.tsx
index eeb9272..8bd22bc 100644
--- a/src/app/(marketing)/services/[id]/client.tsx
+++ b/src/app/(marketing)/services/[id]/client.tsx
@@ -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(null);
const serviceChallenges = challenges[service.id as keyof typeof challenges] || [];
@@ -106,12 +107,14 @@ export function ServiceDetailClient({ service }: ServiceDetailClientProps) {
-
-
-
+
diff --git a/src/app/news/[slug]/NewsDetailClient.tsx b/src/app/news/[slug]/NewsDetailClient.tsx
index 68d55b5..8d9a0f5 100644
--- a/src/app/news/[slug]/NewsDetailClient.tsx
+++ b/src/app/news/[slug]/NewsDetailClient.tsx
@@ -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) {
-
router.back()}
>
- 返回新闻列表
-
+ 返回
+
{news.category}
diff --git a/src/app/products/[id]/page.tsx b/src/app/products/[id]/page.tsx
index 488a7b8..29f78fd 100644
--- a/src/app/products/[id]/page.tsx
+++ b/src/app/products/[id]/page.tsx
@@ -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<{
-
-
- 返回产品列表
-
+
{product.category}
diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx
index c17c90a..7ab32fe 100644
--- a/src/components/layout/header.tsx
+++ b/src/components/layout/header.tsx
@@ -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
(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 (
<>
{item.label}
- {(pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href))) && (
+ {isActive(item) && (
+
@@ -32,7 +32,7 @@ export function ServicesSection() {
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto mb-16"
>
-
+
我们的 核心业务
diff --git a/src/components/ui/back-button.tsx b/src/components/ui/back-button.tsx
new file mode 100644
index 0000000..460b82b
--- /dev/null
+++ b/src/components/ui/back-button.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index ab3ea29..6a51014 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -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