From 060566fd7366496410ab93c7f0868b6dcd0da705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Thu, 5 Mar 2026 14:23:19 +0800 Subject: [PATCH] chore: clean up mobile test files and update components --- docs/mobile-test-evaluation-report.md | 464 ---------- ...026-03-05-mobile-automation-test-design.md | 557 ------------ ...6-03-05-mobile-test-implementation-plan.md | 807 ------------------ e2e/src/pages/BasePage.ts | 138 --- e2e/src/pages/MobilePage.ts | 128 --- e2e/src/tests/mobile/mobile-contact.spec.ts | 403 --------- .../tests/mobile/mobile-functionality.spec.ts | 358 -------- e2e/src/utils/DeviceMatrix.ts | 127 --- e2e/src/utils/MobileHelper.ts | 270 ------ e2e/src/utils/PerformanceMonitor.ts | 197 ----- e2e/src/utils/TestDataGenerator.ts | 343 -------- src/app/globals.css | 32 +- src/components/layout/breadcrumb.tsx | 23 +- 13 files changed, 9 insertions(+), 3838 deletions(-) delete mode 100644 docs/mobile-test-evaluation-report.md delete mode 100644 docs/plans/2026-03-05-mobile-automation-test-design.md delete mode 100644 docs/plans/2026-03-05-mobile-test-implementation-plan.md delete mode 100644 e2e/src/pages/MobilePage.ts delete mode 100644 e2e/src/tests/mobile/mobile-contact.spec.ts delete mode 100644 e2e/src/tests/mobile/mobile-functionality.spec.ts delete mode 100644 e2e/src/utils/DeviceMatrix.ts delete mode 100644 e2e/src/utils/MobileHelper.ts diff --git a/docs/mobile-test-evaluation-report.md b/docs/mobile-test-evaluation-report.md deleted file mode 100644 index b2bc875..0000000 --- a/docs/mobile-test-evaluation-report.md +++ /dev/null @@ -1,464 +0,0 @@ -# 移动端自动化测试现状评估报告 - -**评估时间**: 2026-03-05 -**评估者**: 张翔 -**测试框架**: Playwright + TypeScript -**评估范围**: 移动端E2E测试 - ---- - -## 一、执行摘要 - -### 1.1 测试执行概况 - -**测试环境** -- 浏览器:Mobile Chrome (Pixel 5)、Mobile Safari (iPhone 12) -- 基础URL:http://localhost:3001 -- 测试框架:Playwright 1.58.2 + TypeScript - -**总体统计** -- 总测试数:470个(所有项目) -- 移动端测试:60个 -- 通过:55个(91.7%) -- 失败:5个(8.3%) -- 执行时间:约4.8分钟 - -### 1.2 关键发现 - -**✅ 优势** -1. 移动端测试通过率较高(91.7%) -2. 基础功能测试覆盖较完整 -3. 测试框架结构清晰 -4. 已有页面对象模式基础 - -**❌ 问题** -1. 可访问性问题严重(58个颜色对比度违规) -2. 面包屑导航在移动端缺失 -3. 冒烟测试在移动端失败率较高(33个失败) -4. 性能测试通过率极低(9.1%) -5. 响应式测试通过率低(26.7%) - ---- - -## 二、详细测试结果分析 - -### 2.1 移动端测试结果 - -**测试文件**: `src/tests/mobile/mobile-ux.spec.ts` - -**测试用例统计** -- 总用例数:13个 -- 通过用例:12个 -- 失败用例:1个 -- 通过率:92.3% - -**失败用例详情** -``` -测试名称: Mobile About page renders correctly -失败原因: 面包屑导航元素未找到 -选择器: nav[aria-label="breadcrumb"] -错误信息: element(s) not found -影响设备: Chromium, Firefox, WebKit, Mobile Chrome, Mobile Safari -``` - -**通过用例列表** -1. ✅ Mobile menu opens and closes correctly -2. ✅ Mobile menu navigation works -3. ✅ Mobile menu closes on outside click -4. ✅ Mobile viewport renders correctly -5. ✅ Touch targets are appropriately sized -6. ✅ Mobile page scrolls smoothly -7. ✅ Mobile images are responsive -8. ✅ Mobile text is readable -9. ✅ Mobile Products page cards stack vertically -10. ✅ Mobile Contact page form is usable -11. ✅ Mobile keyboard navigation works - -### 2.2 冒烟测试结果 - -**测试文件**: `src/tests/smoke/*.spec.ts` - -**测试统计** -- 总用例数:57个 -- 通过用例:24个 -- 失败用例:33个 -- 通过率:42.1% - -**失败用例分布** -- 联系页面冒烟测试:20个失败 -- 首页冒烟测试:8个失败 -- 导航冒烟测试:5个失败 - -**典型失败原因** -1. 元素选择器在移动端不匹配 -2. 元素在移动端不可见(响应式隐藏) -3. 移动端布局变化导致定位失败 -4. 超时问题(移动端加载较慢) - -### 2.3 可访问性测试结果 - -**测试文件**: `src/tests/accessibility/wcag-compliance.spec.ts` - -**违规统计** -- 总违规数:58个 -- 严重程度:serious -- 主要类型:color-contrast(颜色对比度) - -**典型违规示例** -``` -元素:

扫码关注获取最新资讯

-前景色: #718096 -背景色: #F5F5F5 -对比度: 3.68:1 -要求: 4.5:1 (WCAG 2.1 AA) -影响: 页脚区域文本可读性 -``` - -**影响范围** -- 页脚区域:版权信息、社交媒体链接 -- 联系信息:辅助文本、提示信息 -- 表单标签:占位符文本、帮助文本 - -### 2.4 性能测试结果 - -**测试文件**: `src/tests/performance/*.spec.ts` - -**测试统计** -- 总用例数:33个 -- 通过用例:3个 -- 失败用例:30个 -- 通过率:9.1% - -**失败原因分析** -1. 页面加载时间超出阈值(LCP > 2.5s) -2. 交互响应时间过长(FID > 100ms) -3. 滚动性能不佳(帧率 < 60fps) -4. 资源加载效率低(TTFB > 600ms) - -**性能指标现状** -``` -LCP (最大内容绘制): 3.2s (目标: <2.5s) -FID (首次输入延迟): 180ms (目标: <100ms) -CLS (累积布局偏移): 0.15 (目标: <0.1) -TTI (可交互时间): 4.5s (目标: <3.5s) -TTFB (首字节时间): 800ms (目标: <600ms) -``` - -### 2.5 响应式测试结果 - -**测试文件**: `src/tests/responsive/*.spec.ts` - -**测试统计** -- 总用例数:60个 -- 通过用例:16个 -- 失败用例:44个 -- 通过率:26.7% - -**失败原因分析** -1. 移动端布局适配不完善 -2. 平板端显示需要调整 -3. 大屏幕显示需要优化 -4. 移动端交互需要改进 - ---- - -## 三、问题分类与优先级 - -### 3.1 高优先级问题(P0) - -**1. 可访问性问题** -- **问题**: 58个颜色对比度违规 -- **影响**: 影响视觉障碍用户,不符合WCAG 2.1 AA标准 -- **解决方案**: - - 调整文本颜色或背景色 - - 增加字体大小或字重 - - 使用更深的文本颜色 -- **预计工作量**: 2天 - -**2. 面包屑导航缺失** -- **问题**: 移动端About页面缺少面包屑导航 -- **影响**: 用户无法了解当前位置,导航体验差 -- **解决方案**: - - 在移动端添加面包屑导航 - - 或在移动端使用其他导航方式 -- **预计工作量**: 0.5天 - -**3. 冒烟测试失败** -- **问题**: 33个冒烟测试在移动端失败 -- **影响**: 基础功能验证不完整,无法保证质量 -- **解决方案**: - - 修复元素选择器 - - 适配移动端布局变化 - - 优化超时设置 -- **预计工作量**: 3天 - -### 3.2 中优先级问题(P1) - -**1. 性能问题** -- **问题**: 性能测试通过率仅9.1% -- **影响**: 用户体验差,影响SEO排名 -- **解决方案**: - - 优化图片加载(懒加载、WebP格式) - - 代码分割和按需加载 - - 优化CSS和JS - - 改善缓存策略 -- **预计工作量**: 5天 - -**2. 响应式适配问题** -- **问题**: 响应式测试通过率仅26.7% -- **影响**: 不同设备显示效果不一致 -- **解决方案**: - - 完善移动端布局 - - 优化平板端显示 - - 改善大屏幕体验 -- **预计工作量**: 4天 - -### 3.3 低优先级问题(P2) - -**1. 测试覆盖不足** -- **问题**: 移动端测试覆盖率仅30% -- **影响**: 无法全面保证移动端质量 -- **解决方案**: - - 扩展移动端测试用例 - - 增加设备覆盖 - - 添加更多测试场景 -- **预计工作量**: 10天 - ---- - -## 四、测试框架评估 - -### 4.1 架构优势 - -**1. 清晰的目录结构** -``` -e2e/src/ -├── pages/ # 页面对象 -├── tests/ # 测试用例 -├── fixtures/ # 测试夹具 -├── utils/ # 工具类 -└── config/ # 配置文件 -``` - -**2. 页面对象模式** -- BasePage提供通用方法 -- 各页面继承BasePage -- 代码复用性好 - -**3. 测试分类清晰** -- Smoke测试 -- Regression测试 -- Performance测试 -- Responsive测试 -- Mobile测试 -- Accessibility测试 -- Security测试 -- Visual测试 - -### 4.2 架构不足 - -**1. 缺少移动端专用页面对象** -- 当前只有通用的BasePage -- 没有MobilePage专门处理移动端交互 -- 移动端特定方法分散在各测试中 - -**2. 设备矩阵不完善** -- 只配置了Pixel 5和iPhone 12 -- 缺少其他主流设备 -- 没有响应式断点测试 - -**3. 性能监控不完整** -- PerformanceMonitor功能有限 -- 缺少Core Web Vitals完整监控 -- 没有移动端特定性能指标 - -**4. 测试数据管理缺失** -- 没有统一的测试数据管理 -- 测试数据硬编码在测试中 -- 缺少测试数据生成工具 - ---- - -## 五、与设计方案的差距分析 - -### 5.1 测试覆盖率差距 - -| 测试类型 | 设计目标 | 当前状态 | 差距 | -|---------|---------|---------|------| -| 基础功能测试 | 90%+ | 42.1% | -47.9% | -| 性能测试 | 90%+ | 9.1% | -80.9% | -| 兼容性测试 | 95%+ | 26.7% | -68.3% | -| 可访问性测试 | 90%+ | 0% | -90% | -| 用户体验测试 | 90%+ | 92.3% | +2.3% | - -### 5.2 设备覆盖差距 - -| 设备类型 | 设计目标 | 当前状态 | 差距 | -|---------|---------|---------|------| -| iPhone 12/14 | ✅ | ✅ | - | -| Galaxy S21 | ✅ | ❌ | -1 | -| iPad Pro | ✅ | ❌ | -1 | -| iPad Mini | ✅ | ❌ | -1 | -| 响应式断点 | ✅ | ❌ | -5 | - -### 5.3 技术实现差距 - -| 功能模块 | 设计要求 | 当前状态 | 差距 | -|---------|---------|---------|------| -| MobilePage | ✅ | ❌ | 未创建 | -| DeviceMatrix | ✅ | ❌ | 未创建 | -| PerformanceMonitor增强 | ✅ | ⚠️ | 部分实现 | -| TestDataGenerator | ✅ | ⚠️ | 部分实现 | -| CI/CD集成 | ✅ | ⚠️ | 部分实现 | - ---- - -## 六、改进建议 - -### 6.1 立即行动项(1周内) - -**1. 修复高优先级问题** -- 修复可访问性问题(颜色对比度) -- 添加移动端面包屑导航 -- 修复冒烟测试失败 - -**2. 完善测试基础设施** -- 创建MobilePage页面对象 -- 配置智能设备矩阵 -- 增强PerformanceMonitor - -**3. 优化测试执行** -- 修复元素选择器 -- 优化超时设置 -- 改善测试稳定性 - -### 6.2 短期改进项(2-4周) - -**1. 性能优化** -- 优化图片加载 -- 实施代码分割 -- 改善缓存策略 -- 优化CSS和JS - -**2. 响应式改进** -- 完善移动端布局 -- 优化平板端显示 -- 改善大屏幕体验 - -**3. 测试扩展** -- 扩展移动端测试用例 -- 增加设备覆盖 -- 添加更多测试场景 - -### 6.3 长期优化项(1-3个月) - -**1. 建立持续优化机制** -- 定期评估测试结果 -- 动态调整设备矩阵 -- 持续优化性能 - -**2. 引入高级测试技术** -- AI驱动的测试用例生成 -- 测试即代码(TaaC) -- 智能测试调度 - -**3. 建立质量度量体系** -- 测试覆盖率监控 -- 缺陷逃逸率分析 -- 质量趋势分析 - ---- - -## 七、风险评估 - -### 7.1 技术风险 - -**高风险** -- 可访问性问题可能导致法律风险 -- 性能问题影响用户体验和SEO -- 响应式问题影响多设备兼容性 - -**中风险** -- 测试覆盖不足可能导致缺陷逃逸 -- 设备覆盖不足可能导致兼容性问题 -- 测试稳定性不足可能导致误报 - -### 7.2 项目风险 - -**时间风险** -- 修复高优先级问题需要5.5天 -- 完成短期改进项需要2-4周 -- 完成长期优化项需要1-3个月 - -**资源风险** -- 需要开发资源修复问题 -- 需要测试资源扩展测试 -- 需要运维资源优化性能 - -### 7.3 应对措施 - -**技术应对** -- 优先修复高优先级问题 -- 建立持续优化机制 -- 定期评估和调整 - -**项目应对** -- 分阶段实施,降低风险 -- 合理分配资源 -- 建立应急预案 - ---- - -## 八、总结 - -### 8.1 现状总结 - -**优势** -1. 测试框架基础良好 -2. 移动端测试通过率较高 -3. 测试结构清晰 -4. 已有页面对象模式 - -**不足** -1. 可访问性问题严重 -2. 性能问题突出 -3. 响应式适配不完善 -4. 测试覆盖不足 - -### 8.2 改进方向 - -**短期(1-2周)** -- 修复高优先级问题 -- 完善测试基础设施 -- 优化测试执行 - -**中期(1-2个月)** -- 性能优化 -- 响应式改进 -- 测试扩展 - -**长期(3-6个月)** -- 建立持续优化机制 -- 引入高级测试技术 -- 建立质量度量体系 - -### 8.3 预期成果 - -**量化指标** -- 移动端测试覆盖率:从30%提升到90%+ -- 测试通过率:从91.7%提升到98%+ -- 性能测试通过率:从9.1%提升到90%+ -- 响应式测试通过率:从26.7%提升到90%+ - -**质量提升** -- 可访问性:符合WCAG 2.1 AA标准 -- 性能:Core Web Vitals达到优秀水平 -- 兼容性:覆盖95%用户 -- 用户体验:触摸交互和可访问性得到保障 - ---- - -**报告版本**: v1.0 -**最后更新**: 2026-03-05 -**维护者**: 张翔 -**状态**: 已完成 diff --git a/docs/plans/2026-03-05-mobile-automation-test-design.md b/docs/plans/2026-03-05-mobile-automation-test-design.md deleted file mode 100644 index be820d8..0000000 --- a/docs/plans/2026-03-05-mobile-automation-test-design.md +++ /dev/null @@ -1,557 +0,0 @@ -# 移动端自动化测试完善方案设计 - -**创建时间**: 2026-03-05 -**设计者**: 张翔 -**状态**: 已确认 -**版本**: v1.0 - ---- - -## 一、项目背景 - -### 1.1 当前状态 - -**技术栈** -- Next.js 16 + React 19 + TypeScript -- Playwright E2E测试框架 -- 已有基础移动端测试(覆盖率30%) - -**现有测试类型** -- Smoke测试(4个文件) -- 回归测试(2个文件) -- 性能测试(3个文件) -- 响应式测试(2个文件) -- 视觉测试(2个文件) -- 移动端测试(1个文件,13个用例) -- 安全测试(3个文件) -- 可访问性测试(2个文件) - -### 1.2 存在问题 - -1. **测试覆盖不足** - 移动端测试覆盖率仅30% -2. **设备覆盖有限** - 仅配置Pixel 5和iPhone 12 -3. **性能测试不完整** - 缺少Core Web Vitals完整测试 -4. **触摸交互测试缺失** - 没有手势操作测试 -5. **可访问性测试不足** - 缺少屏幕阅读器兼容性测试 - -### 1.3 改进目标 - -- 移动端测试覆盖率:从30%提升到90%+ -- 设备覆盖率:覆盖95%用户 -- 测试稳定性:失败率<2% -- 测试执行时间:15-60分钟(根据测试类型) - ---- - -## 二、设计原则 - -### 2.1 分层渐进策略 - -**测试金字塔原则** -- 70% 基础功能测试:确保所有页面核心功能正常 -- 20% 性能/兼容性测试:覆盖不同设备和浏览器的性能表现 -- 10% 深度用户体验测试:触摸手势、可访问性、边缘场景 - -### 2.2 智能设备矩阵 - -**设备覆盖策略** -- 核心设备:iPhone 12/14、Samsung Galaxy S21、iPad(覆盖70%用户) -- 关键断点:375px、414px、768px、1024px、1280px -- 动态调整:根据用户访问数据定期更新设备矩阵 - -### 2.3 完全自动化 - -**自动化程度** -- 所有测试自动执行 -- 自动生成报告(Allure + HTML + JUnit XML) -- 失败时自动截图和录制视频 -- 通过CI/CD自动运行 - ---- - -## 三、整体架构设计 - -### 3.1 架构分层 - -**1. 测试金字塔层(70%基础功能)** -- 页面对象层:扩展现有的BasePage、ContactPage、HomePage,添加MobilePage专用方法 -- 测试用例层:按页面组织,每个页面包含基础功能测试套件 -- 设备模拟层:使用Playwright的devices配置,覆盖核心移动设备 - -**2. 性能兼容层(20%性能/兼容性)** -- 性能监控:集成PerformanceMonitor,测量LCP、FID、CLS等Core Web Vitals -- 设备矩阵:iPhone 12/14、Samsung Galaxy S21、iPad + 关键响应式断点 -- 网络模拟:测试3G、4G、WiFi不同网络条件下的表现 - -**3. 深度体验层(10%用户体验)** -- 触摸交互:手势操作、触摸目标大小、多点触控 -- 可访问性:WCAG 2.1 AA合规、屏幕阅读器兼容、键盘导航 -- 边缘场景:弱网、低电量、后台切换等 - -**4. 自动化执行层** -- CI/CD集成:GitLab CI自动运行测试 -- 报告系统:Allure报告 + HTML报告 + JUnit XML -- 失败处理:自动截图、录制视频、生成错误日志 - -### 3.2 目录结构 - -``` -e2e/src/ -├── pages/ -│ ├── BasePage.ts # 基础页面对象(扩展) -│ ├── MobilePage.ts # 移动端专用页面对象(新增) -│ ├── ContactPage.ts # 联系页(扩展) -│ ├── HomePage.ts # 首页(扩展) -│ ├── ProductsPage.ts # 产品页(扩展) -│ ├── ServicesPage.ts # 服务页(扩展) -│ └── AboutPage.ts # 关于页(扩展) -├── tests/ -│ ├── mobile/ -│ │ ├── mobile-ux.spec.ts # 移动端用户体验(扩展) -│ │ ├── mobile-functionality.spec.ts # 移动端功能测试(新增) -│ │ ├── mobile-performance.spec.ts # 移动端性能测试(新增) -│ │ ├── mobile-compatibility.spec.ts # 移动端兼容性测试(新增) -│ │ ├── mobile-touch.spec.ts # 触摸交互测试(新增) -│ │ └── mobile-accessibility.spec.ts # 移动端可访问性(新增) -│ ├── performance/ -│ │ └── core-web-vitals.spec.ts # Core Web Vitals(新增) -│ └── responsive/ -│ └── device-matrix.spec.ts # 设备矩阵测试(新增) -├── utils/ -│ ├── PerformanceMonitor.ts # 性能监控(扩展) -│ ├── TestDataGenerator.ts # 测试数据生成(扩展) -│ ├── DeviceMatrix.ts # 设备矩阵配置(新增) -│ └── MobileHelper.ts # 移动端辅助工具(新增) -└── fixtures/ - ├── base.fixture.ts # 基础fixture(扩展) - └── mobile.fixture.ts # 移动端fixture(新增) -``` - ---- - -## 四、测试用例设计 - -### 4.1 基础功能测试(70%) - -**首页测试套件** -- Hero区域:标题、描述、CTA按钮可见性、响应式布局 -- 导航测试:移动菜单开关、导航链接、面包屑导航 -- 内容区域:服务、产品、案例、新闻卡片的响应式布局 -- 页脚测试:联系信息、社交媒体链接、版权信息 -- 表单交互:快速联系表单的移动端适配 - -**联系页测试套件** -- 表单字段:姓名、邮箱、电话、消息字段的移动端输入 -- 表单验证:必填字段、格式验证、错误提示显示 -- 提交流程:表单提交、成功消息、错误处理 -- 联系信息:地址、电话、邮箱的可点击性 -- 地图集成:移动端地图显示和交互 - -**产品/服务页测试套件** -- 列表展示:卡片堆叠布局、图片响应式、文本可读性 -- 详情页:产品详情、服务描述的移动端适配 -- 筛选功能:移动端筛选器交互 -- 分页导航:移动端分页控件 - -### 4.2 性能兼容测试(20%) - -**Core Web Vitals** -- LCP(最大内容绘制):首屏加载时间 < 2.5秒 -- FID(首次输入延迟):交互响应时间 < 100毫秒 -- CLS(累积布局偏移):布局稳定性 < 0.1 - -**设备兼容性** -- iPhone 12/14:iOS Safari浏览器测试 -- Samsung Galaxy S21:Android Chrome浏览器测试 -- iPad:平板横竖屏切换测试 -- 响应式断点:375px、414px、768px、1024px - -**网络条件** -- 3G网络:慢速网络下的加载表现 -- 4G网络:正常移动网络下的性能 -- WiFi:高速网络下的性能表现 - -### 4.3 深度体验测试(10%) - -**触摸交互** -- 触摸目标:最小44x44px,符合WCAG标准 -- 手势操作:滑动、长按、双击、捏合缩放 -- 多点触控:双指滚动、三指手势 - -**可访问性** -- 屏幕阅读器:VoiceOver、TalkBack兼容性 -- 键盘导航:Tab键导航、焦点管理 -- 颜色对比度:文本与背景对比度 ≥ 4.5:1 - -**边缘场景** -- 弱网环境:超时处理、重试机制 -- 后台切换:页面状态保持 -- 低电量模式:性能降级表现 - ---- - -## 五、技术实现 - -### 5.1 页面对象增强 - -**BasePage扩展** -```typescript -// 新增移动端专用方法 -- tapElement(): 模拟触摸点击 -- swipe(): 模拟滑动操作 -- longPress(): 模拟长按操作 -- pinchZoom(): 模拟捏合缩放 -- waitForMobileLoad(): 移动端加载等待 -- checkTouchTarget(): 验证触摸目标大小 -- measurePerformance(): 性能指标测量 -- captureMobileScreenshot(): 移动端截图 -``` - -**MobilePage新建** -```typescript -// 专门的移动端页面对象 -- handleMobileMenu(): 移动菜单操作 -- handleMobileNavigation(): 移动导航 -- handleMobileForm(): 移动表单交互 -- handleMobileScroll(): 移动滚动行为 -- handleMobileGestures(): 移动手势操作 -``` - -### 5.2 设备矩阵配置 - -**智能设备矩阵** -```typescript -const mobileDevices = { - // 核心设备(覆盖70%用户) - core: [ - 'iPhone 12', // iOS主流 - 'iPhone 14', // iOS最新 - 'Galaxy S21', // Android主流 - 'iPad Pro', // 平板横屏 - 'iPad Mini' // 平板竖屏 - ], - // 响应式断点 - breakpoints: [ - { width: 375, height: 667 }, // iPhone SE - { width: 414, height: 896 }, // iPhone 11 Pro Max - { width: 768, height: 1024 }, // iPad竖屏 - { width: 1024, height: 768 }, // iPad横屏 - { width: 1280, height: 720 } // 小屏笔记本 - ] -}; -``` - -### 5.3 性能监控集成 - -**PerformanceMonitor增强** -```typescript -// Core Web Vitals监控 -- measureLCP(): 最大内容绘制 -- measureFID(): 首次输入延迟 -- measureCLS(): 累积布局偏移 -- measureTTI(): 可交互时间 -- measureTTFB(): 首字节时间 - -// 移动端特定指标 -- measureFCP(): 首次内容绘制 -- measureFMP(): 首次有意义绘制 -- measureNetwork(): 网络性能 -- measureBattery(): 电池消耗(模拟) -``` - -### 5.4 测试数据管理 - -**TestDataGenerator扩展** -```typescript -// 移动端测试数据 -- generateMobileFormData(): 移动端表单数据 -- generateMobileUserAgent(): 移动设备User-Agent -- generateNetworkConditions(): 网络条件模拟 -- generateTouchEvents(): 触摸事件数据 -``` - ---- - -## 六、自动化执行与CI/CD集成 - -### 6.1 测试执行策略 - -**分层执行计划** -```bash -# 快速冒烟测试(每次提交) -npm run test:mobile:smoke -- 核心功能基础测试 -- 2-3个主流设备 -- 执行时间:2-3分钟 - -# 完整功能测试(PR合并) -npm run test:mobile:full -- 所有功能测试 -- 核心设备矩阵 -- 执行时间:10-15分钟 - -# 性能兼容测试(每日构建) -npm run test:mobile:performance -- 性能指标测试 -- 完整设备矩阵 -- 执行时间:20-30分钟 - -# 全面回归测试(发布前) -npm run test:mobile:regression -- 所有测试套件 -- 智能设备矩阵 -- 执行时间:45-60分钟 -``` - -### 6.2 GitLab CI配置 - -**CI/CD Pipeline** -```yaml -stages: - - smoke - - full - - performance - - regression - -# 冒烟测试(每次提交) -mobile:smoke: - stage: smoke - script: - - cd e2e - - npm install - - npm run test:mobile:smoke - artifacts: - when: always - paths: - - e2e/test-results/ - - e2e/allure-results/ - only: - - branches - -# 完整测试(PR) -mobile:full: - stage: full - script: - - cd e2e - - npm run test:mobile:full - artifacts: - when: always - paths: - - e2e/test-results/ - - e2e/allure-results/ - only: - - merge_requests - -# 性能测试(每日) -mobile:performance: - stage: performance - script: - - cd e2e - - npm run test:mobile:performance - artifacts: - when: always - paths: - - e2e/test-results/ - - e2e/allure-results/ - only: - - schedules - -# 回归测试(发布) -mobile:regression: - stage: regression - script: - - cd e2e - - npm run test:mobile:regression - artifacts: - when: always - paths: - - e2e/test-results/ - - e2e/allure-results/ - only: - - tags -``` - -### 6.3 报告系统 - -**多格式报告** -```typescript -// Allure报告(详细) -- 测试用例详情 -- 失败截图和视频 -- 性能指标图表 -- 设备覆盖率统计 - -// HTML报告(快速查看) -- 测试执行摘要 -- 失败用例列表 -- 趋势分析图表 - -// JUnit XML(CI集成) -- 测试结果XML -- 失败用例标记 -- 执行时间统计 - -// JSON报告(数据分析) -- 测试结果JSON -- 性能数据JSON -- 设备矩阵JSON -``` - -### 6.4 失败处理机制 - -**自动失败处理** -```typescript -// 失败时自动执行 -- 截图:失败页面截图 -- 录制:失败过程视频 -- 日志:详细错误日志 -- 网络请求:失败时的网络请求记录 -- 控制台日志:浏览器控制台错误 -- 性能数据:失败时的性能指标 - -// 失败重试策略 -- 轻量测试:重试2次 -- 重量测试:重试1次 -- 性能测试:不重试 -``` - ---- - -## 七、实施计划 - -### 7.1 分阶段实施计划 - -**第一阶段:基础设施完善(3天)** -- Day 1: 扩展BasePage,添加移动端专用方法 -- Day 2: 创建MobilePage页面对象,配置智能设备矩阵 -- Day 3: 增强PerformanceMonitor,建立测试数据管理 - -**第二阶段:核心测试开发(5天)** -- Day 1-2: 首页移动端测试套件(基础功能) -- Day 3: 联系页移动端测试套件(表单交互) -- Day 4: 产品/服务页移动端测试套件(响应式布局) -- Day 5: 导航和通用组件测试 - -**第三阶段:性能兼容测试(3天)** -- Day 1: Core Web Vitals测试 -- Day 2: 设备兼容性测试 -- Day 3: 网络条件测试 - -**第四阶段:深度体验测试(2天)** -- Day 1: 触摸交互测试 -- Day 2: 可访问性测试 - -**第五阶段:CI/CD集成(2天)** -- Day 1: GitLab CI配置 -- Day 2: 报告系统和失败处理 - -**总计:15天(3周)** - -### 7.2 质量保障措施 - -**代码质量** -- TypeScript严格模式:100%类型安全 -- ESLint规则:遵循项目规范 -- 代码审查:所有测试代码需要审查 - -**测试质量** -- 测试覆盖率:目标90%以上 -- 测试稳定性:失败率<2% -- 测试性能:单次执行<60分钟 - -**文档质量** -- 测试用例文档:每个测试用例都有说明 -- 测试报告:详细的测试结果分析 -- 维护文档:测试框架使用指南 - -### 7.3 持续优化机制 - -**定期评估** -- 每周:测试执行结果分析 -- 每月:设备矩阵更新(基于用户数据) -- 每季度:测试策略评估和优化 - -**智能调整** -- 根据失败率调整重试策略 -- 根据执行时间优化测试顺序 -- 根据用户数据调整设备矩阵 - -**性能监控** -- 测试执行时间趋势 -- 测试稳定性趋势 -- 设备覆盖率趋势 - -### 7.4 风险控制 - -**风险识别** -- 测试环境不稳定:使用容器化环境 -- 测试数据污染:每次测试前清理数据 -- 设备模拟不准确:定期验证设备配置 -- 测试执行超时:设置合理超时时间 - -**应急预案** -- 测试失败:自动通知开发团队 -- 性能下降:触发性能优化流程 -- 兼容性问题:标记为高优先级修复 -- CI/CD失败:阻止代码合并 - ---- - -## 八、预期成果 - -### 8.1 量化指标 - -- **测试覆盖率**:从30%提升到90%+ -- **设备覆盖率**:覆盖95%用户 -- **测试稳定性**:失败率<2% -- **测试执行时间**:15-60分钟(根据测试类型) -- **Core Web Vitals**:LCP<2.5s, FID<100ms, CLS<0.1 - -### 8.2 质量提升 - -- **功能完整性**:所有移动端功能都有测试覆盖 -- **性能优化**:性能问题提前发现和修复 -- **兼容性保障**:主流设备兼容性得到验证 -- **用户体验**:触摸交互和可访问性得到保障 - -### 8.3 效率提升 - -- **自动化程度**:100%自动化执行 -- **反馈速度**:2-3分钟快速反馈 -- **维护成本**:降低人工测试成本 -- **发布信心**:提升发布质量信心 - ---- - -## 九、总结 - -### 9.1 核心优势 - -1. ✅ **全面覆盖**:功能、性能、兼容性、用户体验四个维度 -2. ✅ **分层渐进**:70%基础+20%性能+10%深度,资源利用最优 -3. ✅ **智能设备矩阵**:结合设备覆盖和响应式断点 -4. ✅ **完全自动化**:CI/CD集成,自动报告,失败处理 -5. ✅ **持续优化**:基于数据的智能调整机制 - -### 9.2 关键成功因素 - -- 严格的分层测试策略 -- 智能的设备矩阵管理 -- 完善的自动化执行机制 -- 持续的监控和优化 -- 有效的风险控制 - -### 9.3 后续优化方向 - -- 引入AI驱动的测试用例生成 -- 实现测试即代码(TaaC) -- 扩展到更多移动端场景 -- 集成更多性能监控工具 -- 建立测试数据驱动的决策机制 - ---- - -**文档版本**: v1.0 -**最后更新**: 2026-03-05 -**维护者**: 张翔 -**状态**: 已确认,准备实施 diff --git a/docs/plans/2026-03-05-mobile-test-implementation-plan.md b/docs/plans/2026-03-05-mobile-test-implementation-plan.md deleted file mode 100644 index aadc2e1..0000000 --- a/docs/plans/2026-03-05-mobile-test-implementation-plan.md +++ /dev/null @@ -1,807 +0,0 @@ -# 移动端自动化测试改进实施计划 - -**创建时间**: 2026-03-05 -**计划者**: 张翔 -**状态**: 准备执行 -**版本**: v1.0 -**工作目录**: /Users/zhangxiang/Codes/Gitee/home-page/mobile-test-work - ---- - -## 一、项目概述 - -### 1.1 目标 - -基于移动端自动化测试设计方案,系统性地改进移动端测试,实现: - -- 移动端测试覆盖率:从30%提升到90%+ -- 测试通过率:从91.7%提升到98%+ -- 性能测试通过率:从9.1%提升到90%+ -- 响应式测试通过率:从26.7%提升到90%+ -- 可访问性:符合WCAG 2.1 AA标准 - -### 1.2 范围 - -**包含** -- 测试基础设施完善 -- 移动端测试用例开发 -- 性能和兼容性测试 -- 深度用户体验测试 -- CI/CD集成 - -**不包含** -- 生产环境部署 -- 第三方服务集成 -- 非移动端测试优化 - -### 1.3 约束条件 - -**时间约束** -- 总工期:15天(3周) -- 每阶段有明确的交付物 -- 每日任务可独立验收 - -**资源约束** -- 开发人员:1人 -- 测试环境:本地开发环境 -- 设备覆盖:模拟设备(Playwright devices) - -**质量约束** -- 代码必须通过ESLint检查 -- 测试必须通过TypeScript类型检查 -- 所有测试必须稳定运行(失败率<2%) - ---- - -## 二、实施计划 - -### 第一阶段:基础设施完善 + P0问题修复(4天) - -#### Day 1: 扩展BasePage - -**任务清单** -- [ ] 添加移动端专用方法 - - [ ] `tapElement(selector: string): Promise` - 模拟触摸点击 - - [ ] `swipe(start: Point, end: Point): Promise` - 模拟滑动操作 - - [ ] `longPress(selector: string): Promise` - 模拟长按操作 - - [ ] `pinchZoom(selector: string, scale: number): Promise` - 模拟捏合缩放 - - [ ] `waitForMobileLoad(): Promise` - 移动端加载等待 - - [ ] `checkTouchTarget(selector: string): Promise` - 验证触摸目标大小 - - [ ] `captureMobileScreenshot(name: string): Promise` - 移动端截图 - -- [ ] 添加性能测量方法 - - [ ] `measureLCP(): Promise` - 最大内容绘制 - - [ ] `measureFID(): Promise` - 首次输入延迟 - - [ ] `measureCLS(): Promise` - 累积布局偏移 - - [ ] `measureTTI(): Promise` - 可交互时间 - - [ ] `measureTTFB(): Promise` - 首字节时间 - -- [ ] 添加错误处理机制 - - [ ] `retryOperation(operation: () => Promise, maxRetries: number): Promise` - 重试机制 - - [ ] `handleError(error: Error, context: string): void` - 错误处理 - -- [ ] 添加日志记录功能 - - [ ] `logAction(action: string, details: any): void` - 操作日志 - - [ ] `logError(error: Error, context: string): void` - 错误日志 - -**验收标准** -- ✅ 所有新方法都有TypeScript类型定义 -- ✅ 所有方法都有单元测试 -- ✅ 代码通过ESLint检查 -- ✅ 方法文档完整 - -**交付物** -- `e2e/src/pages/BasePage.ts`(扩展版) -- `e2e/src/pages/BasePage.test.ts`(单元测试) - ---- - -#### Day 2: 创建MobilePage和DeviceMatrix - -**任务清单** -- [ ] 创建MobilePage页面对象 - - [ ] `handleMobileMenu(): Promise` - 移动菜单操作 - - [ ] `handleMobileNavigation(): Promise` - 移动导航 - - [ ] `handleMobileForm(): Promise` - 移动表单交互 - - [ ] `handleMobileScroll(): Promise` - 移动滚动行为 - - [ ] `handleMobileGestures(): Promise` - 移动手势操作 - -- [ ] 配置智能设备矩阵 - - [ ] 创建`e2e/src/utils/DeviceMatrix.ts` - - [ ] 配置核心设备:iPhone 12、iPhone 14、Galaxy S21、iPad Pro、iPad Mini - - [ ] 配置响应式断点:375px、414px、768px、1024px、1280px - - [ ] 添加设备选择逻辑 - -- [ ] 创建MobileHelper工具类 - - [ ] `getMobileDevice(name: string): Device` - 获取移动设备配置 - - [ ] `getViewportSize(device: string): Viewport` - 获取视口大小 - - [ ] `isMobileDevice(userAgent: string): boolean` - 判断是否为移动设备 - -**验收标准** -- ✅ MobilePage继承BasePage -- ✅ 设备矩阵包含所有目标设备 -- ✅ 响应式断点配置正确 -- ✅ 所有方法都有文档 - -**交付物** -- `e2e/src/pages/MobilePage.ts` -- `e2e/src/utils/DeviceMatrix.ts` -- `e2e/src/utils/MobileHelper.ts` - ---- - -#### Day 3: 增强PerformanceMonitor和TestDataGenerator - -**任务清单** -- [ ] 增强PerformanceMonitor - - [ ] 添加Core Web Vitals监控方法 - - [ ] `measureLCP(): Promise` - 最大内容绘制 - - [ ] `measureFID(): Promise` - 首次输入延迟 - - [ ] `measureCLS(): Promise` - 累积布局偏移 - - [ ] 添加移动端特定性能指标 - - [ ] `measureFCP(): Promise` - 首次内容绘制 - - [ ] `measureFMP(): Promise` - 首次有意义绘制 - - [ ] `measureNetwork(): Promise` - 网络性能 - - [ ] 添加性能阈值验证 - - [ ] `validateLCP(value: number): boolean` - 验证LCP - - [ ] `validateFID(value: number): boolean` - 验证FID - - [ ] `validateCLS(value: number): boolean` - 验证CLS - -- [ ] 扩展TestDataGenerator - - [ ] 添加移动端测试数据生成 - - [ ] `generateMobileFormData(): FormData` - 移动端表单数据 - - [ ] `generateMobileUserAgent(): string` - 移动设备User-Agent - - [ ] `generateNetworkConditions(): NetworkCondition` - 网络条件模拟 - - [ ] `generateTouchEvents(): TouchEvent[]` - 触摸事件数据 - -- [ ] 创建测试数据文件 - - [ ] `e2e/src/data/mobile-test-data.ts` - 移动端测试数据 - - [ ] `e2e/src/data/performance-thresholds.ts` - 性能阈值配置 - -**验收标准** -- ✅ PerformanceMonitor包含所有Core Web Vitals -- ✅ TestDataGenerator支持移动端数据生成 -- ✅ 性能阈值符合Google标准 -- ✅ 所有方法都有单元测试 - -**交付物** -- `e2e/src/utils/PerformanceMonitor.ts`(增强版) -- `e2e/src/utils/TestDataGenerator.ts`(扩展版) -- `e2e/src/data/mobile-test-data.ts` -- `e2e/src/data/performance-thresholds.ts` - ---- - -#### Day 4: 修复P0问题 - -**任务清单** -- [ ] 修复可访问性问题(颜色对比度) - - [ ] 分析58个颜色对比度违规 - - [ ] 修改页脚区域文本颜色(#718096 → 更深的颜色) - - [ ] 修改联系信息文本颜色 - - [ ] 修改表单标签和占位符颜色 - - [ ] 验证所有修改符合WCAG 2.1 AA标准(对比度≥4.5:1) - -- [ ] 添加移动端面包屑导航 - - [ ] 在About页面添加面包屑导航 - - [ ] 确保面包屑在移动端可见 - - [ ] 添加正确的aria-label - - [ ] 测试面包屑导航功能 - -- [ ] 修复冒烟测试失败 - - [ ] 分析33个失败用例 - - [ ] 修复元素选择器(适配移动端) - - [ ] 优化超时设置(移动端加载较慢) - - [ ] 修复移动端布局变化导致的定位问题 - - [ ] 运行测试验证修复效果 - -**验收标准** -- ✅ 可访问性测试通过(0个违规) -- ✅ 面包屑导航在移动端正常显示 -- ✅ 冒烟测试通过率≥95% -- ✅ 所有修改通过代码审查 - -**交付物** -- 修复后的组件文件 -- 可访问性测试报告 -- 冒烟测试执行报告 - ---- - -### 第二阶段:核心测试开发(5天) - -#### Day 1-2: 首页移动端测试套件 - -**任务清单** -- [ ] 创建`e2e/src/tests/mobile/mobile-functionality.spec.ts` - - [ ] Hero区域测试 - - [ ] 标题可见性和响应式布局 - - [ ] 描述文本可读性 - - [ ] CTA按钮触摸目标大小 - - [ ] CTA按钮点击响应 - - [ ] 导航测试 - - [ ] 移动菜单开关功能 - - [ ] 导航链接点击 - - [ ] 面包屑导航 - - [ ] 内容区域测试 - - [ ] 服务卡片响应式布局 - - [ ] 产品卡片堆叠布局 - - [ ] 案例卡片响应式布局 - - [ ] 新闻卡片响应式布局 - - [ ] 页脚测试 - - [ ] 联系信息可点击性 - - [ ] 社交媒体链接 - - [ ] 版权信息显示 - - [ ] 表单交互测试 - - [ ] 快速联系表单移动端适配 - - [ ] 表单字段输入 - - [ ] 表单提交功能 - -**验收标准** -- ✅ 所有测试用例通过 -- ✅ 测试覆盖首页所有主要功能 -- ✅ 测试执行时间<3分钟 - -**交付物** -- `e2e/src/tests/mobile/mobile-functionality.spec.ts` -- 测试执行报告 - ---- - -#### Day 3: 联系页移动端测试套件 - -**任务清单** -- [ ] 创建`e2e/src/tests/mobile/mobile-contact.spec.ts` - - [ ] 表单字段测试 - - [ ] 姓名字段移动端输入 - - [ ] 电话字段移动端输入 - - [ ] 邮箱字段移动端输入 - - [ ] 主题字段移动端输入 - - [ ] 消息字段移动端输入 - - [ ] 表单验证测试 - - [ ] 必填字段验证 - - [ ] 格式验证(邮箱、电话) - - [ ] 错误提示显示 - - [ ] 错误提示可读性 - - [ ] 提交流程测试 - - [ ] 表单提交功能 - - [ ] 成功消息显示 - - [ ] 错误处理 - - [ ] 提交按钮状态 - - [ ] 联系信息测试 - - [ ] 地址可点击性 - - [ ] 电话可点击性 - - [ ] 邮箱可点击性 - - [ ] 地图集成测试 - - [ ] 地图显示 - - [ ] 地图交互 - -**验收标准** -- ✅ 所有测试用例通过 -- ✅ 测试覆盖联系页所有功能 -- ✅ 测试执行时间<2分钟 - -**交付物** -- `e2e/src/tests/mobile/mobile-contact.spec.ts` -- 测试执行报告 - ---- - -#### Day 4: 产品/服务页移动端测试套件 - -**任务清单** -- [ ] 创建`e2e/src/tests/mobile/mobile-products.spec.ts` - - [ ] 列表展示测试 - - [ ] 产品卡片堆叠布局 - - [ ] 图片响应式加载 - - [ ] 文本可读性 - - [ ] 卡片间距 - - [ ] 详情页测试 - - [ ] 产品详情显示 - - [ ] 服务描述显示 - - [ ] 响应式布局 - - [ ] 筛选功能测试 - - [ ] 移动端筛选器交互 - - [ ] 筛选结果更新 - - [ ] 分页导航测试 - - [ ] 移动端分页控件 - - [ ] 分页功能 - -- [ ] 创建`e2e/src/tests/mobile/mobile-services.spec.ts` - - [ ] 服务列表测试 - - [ ] 服务详情测试 - - [ ] 响应式布局测试 - -**验收标准** -- ✅ 所有测试用例通过 -- ✅ 测试覆盖产品/服务页所有功能 -- ✅ 测试执行时间<3分钟 - -**交付物** -- `e2e/src/tests/mobile/mobile-products.spec.ts` -- `e2e/src/tests/mobile/mobile-services.spec.ts` -- 测试执行报告 - ---- - -#### Day 5: 导航和通用组件测试 - -**任务清单** -- [ ] 创建`e2e/src/tests/mobile/mobile-navigation.spec.ts` - - [ ] 移动菜单测试 - - [ ] 菜单打开/关闭 - - [ ] 菜单项点击 - - [ ] 菜单动画 - - [ ] 菜单滚动 - - [ ] 导航链接测试 - - [ ] 所有导航链接可点击 - - [ ] 导航跳转正确 - - [ ] 导航状态更新 - - [ ] 面包屑导航测试 - - [ ] 面包屑显示 - - [ ] 面包屑点击 - - [ ] 面包屑层级 - -- [ ] 创建`e2e/src/tests/mobile/mobile-components.spec.ts` - - [ ] 按钮组件测试 - - [ ] 触摸目标大小 - - [ ] 按钮状态 - - [ ] 按钮点击响应 - - [ ] 卡片组件测试 - - [ ] 卡片布局 - - [ ] 卡片交互 - - [ ] 表单组件测试 - - [ ] 表单字段布局 - - [ ] 表单验证 - -**验收标准** -- ✅ 所有测试用例通过 -- ✅ 测试覆盖导航和通用组件 -- ✅ 测试执行时间<2分钟 - -**交付物** -- `e2e/src/tests/mobile/mobile-navigation.spec.ts` -- `e2e/src/tests/mobile/mobile-components.spec.ts` -- 测试执行报告 - ---- - -### 第三阶段:性能兼容测试(3天) - -#### Day 1: Core Web Vitals测试 - -**任务清单** -- [ ] 创建`e2e/src/tests/performance/core-web-vitals.spec.ts` - - [ ] LCP测试 - - [ ] 首页LCP<2.5s - - [ ] 联系页LCP<2.5s - - [ ] 产品页LCP<2.5s - - [ ] FID测试 - - [ ] 首页FID<100ms - - [ ] 联系页FID<100ms - - [ ] 产品页FID<100ms - - [ ] CLS测试 - - [ ] 首页CLS<0.1 - - [ ] 联系页CLS<0.1 - - [ ] 产品页CLS<0.1 - - [ ] TTI测试 - - [ ] 首页TTI<3.5s - - [ ] 联系页TTI<3.5s - - [ ] 产品页TTI<3.5s - - [ ] TTFB测试 - - [ ] 首页TTFB<600ms - - [ ] 联系页TTFB<600ms - - [ ] 产品页TTFB<600ms - -**验收标准** -- ✅ 所有Core Web Vitals测试通过 -- ✅ 性能指标符合Google标准 -- ✅ 测试执行时间<5分钟 - -**交付物** -- `e2e/src/tests/performance/core-web-vitals.spec.ts` -- 性能测试报告 - ---- - -#### Day 2: 设备兼容性测试 - -**任务清单** -- [ ] 创建`e2e/src/tests/responsive/device-matrix.spec.ts` - - [ ] iPhone 12测试 - - [ ] 所有页面显示正常 - - [ ] 所有功能正常 - - [ ] iPhone 14测试 - - [ ] 所有页面显示正常 - - [ ] 所有功能正常 - - [ ] Galaxy S21测试 - - [ ] 所有页面显示正常 - - [ ] 所有功能正常 - - [ ] iPad Pro测试 - - [ ] 横屏显示正常 - - [ ] 竖屏显示正常 - - [ ] iPad Mini测试 - - [ ] 横屏显示正常 - - [ ] 竖屏显示正常 - -- [ ] 响应式断点测试 - - [ ] 375px断点测试 - - [ ] 414px断点测试 - - [ ] 768px断点测试 - - [ ] 1024px断点测试 - - [ ] 1280px断点测试 - -**验收标准** -- ✅ 所有设备测试通过 -- ✅ 所有断点测试通过 -- ✅ 测试执行时间<10分钟 - -**交付物** -- `e2e/src/tests/responsive/device-matrix.spec.ts` -- 设备兼容性测试报告 - ---- - -#### Day 3: 网络条件测试 - -**任务清单** -- [ ] 创建`e2e/src/tests/performance/network-conditions.spec.ts` - - [ ] 3G网络测试 - - [ ] 慢速网络下的加载表现 - - [ ] 超时处理 - - [ ] 重试机制 - - [ ] 4G网络测试 - - [ ] 正常移动网络下的性能 - - [ ] 资源加载优化 - - [ ] WiFi网络测试 - - [ ] 高速网络下的性能 - - [ ] 缓存策略验证 - -**验收标准** -- ✅ 所有网络条件测试通过 -- ✅ 网络降级处理正确 -- ✅ 测试执行时间<5分钟 - -**交付物** -- `e2e/src/tests/performance/network-conditions.spec.ts` -- 网络条件测试报告 - ---- - -### 第四阶段:深度体验测试(2天) - -#### Day 1: 触摸交互测试 - -**任务清单** -- [ ] 创建`e2e/src/tests/mobile/mobile-touch.spec.ts` - - [ ] 触摸目标测试 - - [ ] 所有按钮触摸目标≥44x44px - - [ ] 所有链接触摸目标≥44x44px - - [ ] 所有表单控件触摸目标≥44x44px - - [ ] 手势操作测试 - - [ ] 滑动手势 - - [ ] 长按手势 - - [ ] 双击手势 - - [ ] 捏合缩放手势 - - [ ] 多点触控测试 - - [ ] 双指滚动 - - [ ] 三指手势 - -**验收标准** -- ✅ 所有触摸交互测试通过 -- ✅ 触摸目标符合WCAG标准 -- ✅ 测试执行时间<3分钟 - -**交付物** -- `e2e/src/tests/mobile/mobile-touch.spec.ts` -- 触摸交互测试报告 - ---- - -#### Day 2: 可访问性测试 - -**任务清单** -- [ ] 创建`e2e/src/tests/mobile/mobile-accessibility.spec.ts` - - [ ] 屏幕阅读器测试 - - [ ] VoiceOver兼容性 - - [ ] TalkBack兼容性 - - [ ] ARIA标签正确性 - - [ ] 键盘导航测试 - - [ ] Tab键导航 - - [ ] 焦点管理 - - [ ] 焦点可见性 - - [ ] 颜色对比度测试 - - [ ] 所有文本对比度≥4.5:1 - - [ ] 大文本对比度≥3:1 - - [ ] 图标对比度≥3:1 - -**验收标准** -- ✅ 所有可访问性测试通过 -- ✅ 符合WCAG 2.1 AA标准 -- ✅ 测试执行时间<3分钟 - -**交付物** -- `e2e/src/tests/mobile/mobile-accessibility.spec.ts` -- 可访问性测试报告 - ---- - -### 第五阶段:CI/CD集成(2天) - -#### Day 1: GitLab CI配置 - -**任务清单** -- [ ] 创建`.gitlab-ci.yml` - - [ ] 冒烟测试stage - - [ ] 每次提交运行 - - [ ] 核心功能测试 - - [ ] 2-3个主流设备 - - [ ] 完整测试stage - - [ ] PR合并运行 - - [ ] 所有功能测试 - - [ ] 核心设备矩阵 - - [ ] 性能测试stage - - [ ] 每日构建运行 - - [ ] 性能指标测试 - - [ ] 完整设备矩阵 - - [ ] 回归测试stage - - [ ] 发布前运行 - - [ ] 所有测试套件 - - [ ] 智能设备矩阵 - -- [ ] 配置测试脚本 - - [ ] `npm run test:mobile:smoke` - - [ ] `npm run test:mobile:full` - - [ ] `npm run test:mobile:performance` - - [ ] `npm run test:mobile:regression` - -**验收标准** -- ✅ CI/CD pipeline配置正确 -- ✅ 所有测试脚本可用 -- ✅ 测试执行时间符合预期 - -**交付物** -- `.gitlab-ci.yml` -- `e2e/package.json`(更新scripts) - ---- - -#### Day 2: 报告系统和失败处理 - -**任务清单** -- [ ] 配置多格式报告 - - [ ] Allure报告配置 - - [ ] 测试用例详情 - - [ ] 失败截图和视频 - - [ ] 性能指标图表 - - [ ] 设备覆盖率统计 - - [ ] HTML报告配置 - - [ ] 测试执行摘要 - - [ ] 失败用例列表 - - [ ] 趋势分析图表 - - [ ] JUnit XML配置 - - [ ] 测试结果XML - - [ ] 失败用例标记 - - [ ] 执行时间统计 - - [ ] JSON报告配置 - - [ ] 测试结果JSON - - [ ] 性能数据JSON - - [ ] 设备矩阵JSON - -- [ ] 配置失败处理机制 - - [ ] 自动截图 - - [ ] 失败页面截图 - - [ ] 截图命名规则 - - [ ] 自动录制 - - [ ] 失败过程视频 - - [ ] 视频保存策略 - - [ ] 错误日志 - - [ ] 详细错误日志 - - [ ] 日志格式规范 - - [ ] 网络请求记录 - - [ ] 失败时的网络请求 - - [ ] 请求响应数据 - - [ ] 控制台日志 - - [ ] 浏览器控制台错误 - - [ ] 控制台警告 - - [ ] 性能数据 - - [ ] 失败时的性能指标 - - [ ] 性能快照 - -- [ ] 配置失败重试策略 - - [ ] 轻量测试:重试2次 - - [ ] 重量测试:重试1次 - - [ ] 性能测试:不重试 - -**验收标准** -- ✅ 所有报告格式正常生成 -- ✅ 失败处理机制正常工作 -- ✅ 重试策略配置正确 - -**交付物** -- `e2e/playwright.config.ts`(更新) -- `e2e/allure.config.js` -- 报告系统文档 - ---- - -## 三、质量保障 - -### 3.1 代码质量 - -**TypeScript** -- 所有代码必须通过TypeScript类型检查 -- 使用严格模式(strict: true) -- 避免使用any类型 - -**ESLint** -- 所有代码必须通过ESLint检查 -- 遵循项目代码规范 -- 修复所有警告和错误 - -**代码审查** -- 所有代码必须经过审查 -- 审查重点:功能正确性、代码质量、性能影响 -- 审查通过后方可合并 - -### 3.2 测试质量 - -**测试覆盖率** -- 目标:90%以上 -- 工具:Istanbul/nyc -- 定期检查覆盖率报告 - -**测试稳定性** -- 目标:失败率<2% -- 策略:重试机制、超时优化、选择器优化 -- 定期分析失败原因 - -**测试性能** -- 目标:单次执行<60分钟 -- 策略:并行执行、测试分组、优化等待时间 -- 定期优化测试执行时间 - -### 3.3 文档质量 - -**代码文档** -- 所有公共方法必须有JSDoc注释 -- 复杂逻辑必须有行内注释 -- 保持文档与代码同步 - -**测试文档** -- 每个测试用例必须有说明 -- 测试报告必须详细 -- 维护文档必须完整 - ---- - -## 四、风险管理 - -### 4.1 技术风险 - -**风险1:测试环境不稳定** -- 影响:测试结果不可靠 -- 概率:中 -- 应对:使用容器化环境、定期验证环境配置 - -**风险2:测试数据污染** -- 影响:测试结果不准确 -- 概率:中 -- 应对:每次测试前清理数据、使用独立测试数据 - -**风险3:设备模拟不准确** -- 影响:兼容性问题遗漏 -- 概率:低 -- 应对:定期验证设备配置、补充真实设备测试 - -**风险4:测试执行超时** -- 影响:测试执行时间过长 -- 概率:中 -- 应对:设置合理超时时间、优化等待逻辑 - -### 4.2 项目风险 - -**风险1:时间不足** -- 影响:无法完成所有任务 -- 概率:低 -- 应对:分阶段交付、优先保证核心功能 - -**风险2:资源不足** -- 影响:开发进度延迟 -- 概率:低 -- 应对:合理分配资源、及时调整计划 - -**风险3:需求变更** -- 影响:返工、进度延迟 -- 概率:中 -- 应对:需求评审、变更控制流程 - -### 4.3 应对措施 - -**预防措施** -- 详细的需求分析和设计 -- 充分的测试和验证 -- 定期的进度检查和调整 - -**应急措施** -- 建立应急预案 -- 准备备用方案 -- 及时沟通和协调 - ---- - -## 五、验收标准 - -### 5.1 功能验收 - -- ✅ 所有测试用例通过 -- ✅ 测试覆盖率≥90% -- ✅ 测试稳定性≥98% -- ✅ 测试执行时间<60分钟 - -### 5.2 性能验收 - -- ✅ Core Web Vitals达到优秀水平 -- ✅ LCP<2.5s -- ✅ FID<100ms -- ✅ CLS<0.1 - -### 5.3 兼容性验收 - -- ✅ 覆盖95%用户设备 -- ✅ 所有主流设备测试通过 -- ✅ 所有响应式断点测试通过 - -### 5.4 可访问性验收 - -- ✅ 符合WCAG 2.1 AA标准 -- ✅ 颜色对比度≥4.5:1 -- ✅ 触摸目标≥44x44px -- ✅ 屏幕阅读器兼容 - ---- - -## 六、总结 - -### 6.1 预期成果 - -**量化指标** -- 移动端测试覆盖率:从30%提升到90%+ -- 测试通过率:从91.7%提升到98%+ -- 性能测试通过率:从9.1%提升到90%+ -- 响应式测试通过率:从26.7%提升到90%+ - -**质量提升** -- 可访问性:符合WCAG 2.1 AA标准 -- 性能:Core Web Vitals达到优秀水平 -- 兼容性:覆盖95%用户 -- 用户体验:触摸交互和可访问性得到保障 - -**效率提升** -- 自动化程度:100%自动化执行 -- 反馈速度:2-3分钟快速反馈 -- 维护成本:降低人工测试成本 -- 发布信心:提升发布质量信心 - -### 6.2 关键成功因素 - -- 严格的分阶段实施 -- 智能的设备矩阵管理 -- 完善的自动化执行机制 -- 持续的监控和优化 -- 有效的风险控制 - -### 6.3 后续优化方向 - -- 引入AI驱动的测试用例生成 -- 实现测试即代码(TaaC) -- 扩展到更多移动端场景 -- 集成更多性能监控工具 -- 建立测试数据驱动的决策机制 - ---- - -**计划版本**: v1.0 -**最后更新**: 2026-03-05 -**维护者**: 张翔 -**状态**: 准备执行 diff --git a/e2e/src/pages/BasePage.ts b/e2e/src/pages/BasePage.ts index fd4386c..65aeb91 100644 --- a/e2e/src/pages/BasePage.ts +++ b/e2e/src/pages/BasePage.ts @@ -450,142 +450,4 @@ export class BasePage { }); }); } - - async tapElement(selector: string): Promise { - const element = this.page.locator(selector); - const box = await element.boundingBox(); - - if (box) { - const x = box.x + box.width / 2; - const y = box.y + box.height / 2; - await this.page.touchscreen.tap(x, y); - } else { - await element.click(); - } - } - - async swipe(start: { x: number; y: number }, end: { x: number; y: number }): Promise { - await this.page.touchscreen.tap(start.x, start.y); - await this.page.touchscreen.touchMove(end.x, end.y); - await this.page.touchscreen.touchEnd(); - } - - async longPress(selector: string, duration: number = 1000): Promise { - const element = this.page.locator(selector); - const box = await element.boundingBox(); - - if (box) { - const x = box.x + box.width / 2; - const y = box.y + box.height / 2; - await this.page.touchscreen.tap(x, y); - await this.page.waitForTimeout(duration); - } else { - await element.click(); - } - } - - async pinchZoom(selector: string, scale: number = 1.5): Promise { - const element = this.page.locator(selector); - const box = await element.boundingBox(); - - if (box) { - const centerX = box.x + box.width / 2; - const centerY = box.y + box.height / 2; - - const finger1 = { x: centerX - 50, y: centerY }; - const finger2 = { x: centerX + 50, y: centerY }; - - await this.page.touchscreen.tap(finger1.x, finger1.y); - await this.page.touchscreen.tap(finger2.x, finger2.y); - - const newFinger1 = { x: centerX - 50 / scale, y: centerY }; - const newFinger2 = { x: centerX + 50 / scale, y: centerY }; - - await this.page.touchscreen.touchMove(newFinger1.x, newFinger1.y); - await this.page.touchscreen.touchMove(newFinger2.x, newFinger2.y); - - await this.page.touchscreen.touchEnd(); - await this.page.touchscreen.touchEnd(); - } - } - - async waitForMobileLoad(): Promise { - await this.page.waitForLoadState('domcontentloaded'); - await this.page.waitForTimeout(500); - } - - async checkTouchTarget(selector: string): Promise { - const element = this.page.locator(selector); - const box = await element.boundingBox(); - - if (box) { - return box.width >= 44 && box.height >= 44; - } - - return false; - } - - async captureMobileScreenshot(name: string): Promise { - const screenshotDir = 'test-results/mobile-screenshots'; - if (!fs.existsSync(screenshotDir)) { - fs.mkdirSync(screenshotDir, { recursive: true }); - } - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const filename = `${timestamp}-${name}.png`; - await this.page.screenshot({ - path: path.join(screenshotDir, filename), - fullPage: true - }); - } - - async measureLCP(): Promise { - const vitals = await this.getCoreWebVitals(); - return vitals.largestContentfulPaint; - } - - async measureFID(): Promise { - const vitals = await this.getCoreWebVitals(); - return vitals.firstInputDelay; - } - - async measureCLS(): Promise { - const vitals = await this.getCoreWebVitals(); - return vitals.cumulativeLayoutShift; - } - - async measureTTI(): Promise { - const metrics = await this.measurePerformance(); - return metrics.domContentLoaded; - } - - async measureTTFB(): Promise { - const timing = await this.page.evaluate(() => { - return performance.timing; - }); - return timing.responseStart - timing.navigationStart; - } - - async handleError(error: Error, context: string): Promise { - const errorInfo = { - timestamp: new Date().toISOString(), - context, - message: error.message, - stack: error.stack, - url: this.page.url(), - }; - - await this.log(`Error in ${context}: ${error.message}`, 'error'); - await this.captureMobileScreenshot(`error-${context}`); - } - - async logAction(action: string, details: any): Promise { - const logInfo = { - timestamp: new Date().toISOString(), - action, - details, - url: this.page.url(), - }; - - await this.log(`Action: ${action}`, 'info'); - } } diff --git a/e2e/src/pages/MobilePage.ts b/e2e/src/pages/MobilePage.ts deleted file mode 100644 index 29fa6fc..0000000 --- a/e2e/src/pages/MobilePage.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Page, Locator } from '@playwright/test'; -import { BasePage } from './BasePage'; - -export class MobilePage extends BasePage { - constructor(page: Page) { - super(page); - } - - async handleMobileMenu(): Promise { - const menuButton = this.page.locator('button[aria-label="打开菜单"], button[aria-label="关闭菜单"]'); - - if (await menuButton.isVisible()) { - await this.tapElement('button[aria-label="打开菜单"], button[aria-label="关闭菜单"]'); - await this.page.waitForSelector('#mobile-menu', { state: 'visible' }); - } - } - - async closeMobileMenu(): Promise { - const closeButton = this.page.locator('button[aria-label="关闭菜单"]'); - - if (await closeButton.isVisible()) { - await this.tapElement('button[aria-label="关闭菜单"]'); - await this.page.waitForSelector('#mobile-menu', { state: 'hidden' }); - } - } - - async handleMobileNavigation(linkText: string): Promise { - await this.handleMobileMenu(); - - const link = this.page.locator(`#mobile-menu a:has-text("${linkText}")`); - await this.tapElement(`#mobile-menu a:has-text("${linkText}")`); - - await this.closeMobileMenu(); - } - - async handleMobileForm(formData: Record): Promise { - for (const [fieldName, value] of Object.entries(formData)) { - const field = this.page.locator(`input[name="${fieldName}"], textarea[name="${fieldName}"]`); - - if (await field.isVisible()) { - await field.fill(value); - await this.page.waitForTimeout(100); - } - } - } - - async handleMobileScroll(direction: 'up' | 'down' = 'down'): Promise { - const viewportSize = this.page.viewportSize(); - - if (viewportSize) { - const startY = direction === 'down' ? viewportSize.height * 0.8 : viewportSize.height * 0.2; - const endY = direction === 'down' ? viewportSize.height * 0.2 : viewportSize.height * 0.8; - const centerX = viewportSize.width / 2; - - await this.swipe( - { x: centerX, y: startY }, - { x: centerX, y: endY } - ); - - await this.page.waitForTimeout(500); - } - } - - async handleMobileGestures(): Promise { - await this.logAction('Testing mobile gestures', {}); - - const testElement = this.page.locator('body'); - const box = await testElement.boundingBox(); - - if (box) { - const centerX = box.x + box.width / 2; - const centerY = box.y + box.height / 2; - - await this.swipe( - { x: centerX, y: centerY + 100 }, - { x: centerX, y: centerY - 100 } - ); - - await this.page.waitForTimeout(500); - - await this.swipe( - { x: centerX - 100, y: centerY }, - { x: centerX + 100, y: centerY } - ); - - await this.page.waitForTimeout(500); - } - } - - async verifyMobileMenuOpen(): Promise { - const mobileMenu = this.page.locator('#mobile-menu'); - return await mobileMenu.isVisible(); - } - - async verifyMobileMenuClosed(): Promise { - const mobileMenu = this.page.locator('#mobile-menu'); - return !(await mobileMenu.isVisible()); - } - - async getMobileMenuItems(): Promise { - const items = this.page.locator('#mobile-menu a'); - const count = await items.count(); - const itemTexts: string[] = []; - - for (let i = 0; i < count; i++) { - const text = await items.nth(i).textContent(); - if (text) { - itemTexts.push(text); - } - } - - return itemTexts; - } - - async tapMobileMenuItem(itemText: string): Promise { - const menuItem = this.page.locator(`#mobile-menu a:has-text("${itemText}")`); - await this.tapElement(`#mobile-menu a:has-text("${itemText}")`); - } - - async isMobileMenuVisible(): Promise { - const menuButton = this.page.locator('button[aria-label="打开菜单"]'); - return await menuButton.isVisible(); - } - - async waitForMobileMenuAnimation(): Promise { - await this.page.waitForTimeout(300); - } -} diff --git a/e2e/src/tests/mobile/mobile-contact.spec.ts b/e2e/src/tests/mobile/mobile-contact.spec.ts deleted file mode 100644 index f7f95c7..0000000 --- a/e2e/src/tests/mobile/mobile-contact.spec.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { test, expect, Page } from '@playwright/test'; -import { MobilePage } from '../pages/MobilePage'; -import { TestDataGenerator } from '../utils/TestDataGenerator'; -import { PerformanceMonitor } from '../utils/PerformanceMonitor'; - -test.describe('联系页移动端测试套件 @mobile', () => { - let mobilePage: MobilePage; - let performanceMonitor: PerformanceMonitor; - - test.beforeEach(async ({ page }) => { - mobilePage = new MobilePage(page); - performanceMonitor = new PerformanceMonitor(page); - await page.goto('/contact'); - await page.waitForLoadState('domcontentloaded'); - }); - - test.describe('表单字段测试', () => { - test('姓名字段移动端输入正常', async ({ page }) => { - const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"], input[placeholder*="名字"]'); - - if (await nameInput.count() > 0) { - const name = TestDataGenerator.generateName(); - await nameInput.first().fill(name); - - const value = await nameInput.first().inputValue(); - expect(value).toBe(name); - - const box = await nameInput.first().boundingBox(); - expect(box).toBeTruthy(); - expect(box!.width).toBeGreaterThanOrEqual(200); - } - }); - - test('电话字段移动端输入正常', async ({ page }) => { - const phoneInput = page.locator('input[name="phone"], input[placeholder*="电话"], input[placeholder*="手机"]'); - - if (await phoneInput.count() > 0) { - const phone = TestDataGenerator.generatePhone(); - await phoneInput.first().fill(phone); - - const value = await phoneInput.first().inputValue(); - expect(value).toBe(phone); - - const inputType = await phoneInput.first().getAttribute('type'); - expect(['tel', 'text', 'number']).toContain(inputType); - } - }); - - test('邮箱字段移动端输入正常', async ({ page }) => { - const emailInput = page.locator('input[name="email"], input[placeholder*="邮箱"], input[placeholder*="email"]'); - - if (await emailInput.count() > 0) { - const email = TestDataGenerator.generateEmail(); - await emailInput.first().fill(email); - - const value = await emailInput.first().inputValue(); - expect(value).toBe(email); - - const inputType = await emailInput.first().getAttribute('type'); - expect(inputType).toBe('email'); - } - }); - - test('主题字段移动端输入正常', async ({ page }) => { - const subjectInput = page.locator('input[name="subject"], input[placeholder*="主题"], select[name="subject"]'); - - if (await subjectInput.count() > 0) { - const tagName = await subjectInput.first().evaluate((el) => el.tagName.toLowerCase()); - - if (tagName === 'select') { - await subjectInput.first().selectOption({ index: 1 }); - } else { - const subject = TestDataGenerator.generateSubject(); - await subjectInput.first().fill(subject); - - const value = await subjectInput.first().inputValue(); - expect(value).toBe(subject); - } - } - }); - - test('消息字段移动端输入正常', async ({ page }) => { - const messageInput = page.locator('textarea[name="message"], textarea[placeholder*="消息"], textarea[placeholder*="内容"]'); - - if (await messageInput.count() > 0) { - const message = TestDataGenerator.generateMessage(); - await messageInput.first().fill(message); - - const value = await messageInput.first().inputValue(); - expect(value).toBe(message); - - const box = await messageInput.first().boundingBox(); - expect(box).toBeTruthy(); - expect(box!.height).toBeGreaterThanOrEqual(80); - } - }); - }); - - test.describe('表单验证测试', () => { - test('必填字段验证正常', async ({ page }) => { - const submitButton = page.locator('button[type="submit"], button:has-text("提交"), button:has-text("发送")'); - - if (await submitButton.count() > 0) { - await submitButton.first().click(); - await page.waitForTimeout(500); - - const errorMessages = page.locator('[class*="error"], [class*="invalid"], [role="alert"]'); - const errorCount = await errorMessages.count(); - - expect(errorCount).toBeGreaterThanOrEqual(0); - } - }); - - test('邮箱格式验证正常', async ({ page }) => { - const emailInput = page.locator('input[name="email"], input[placeholder*="邮箱"]'); - - if (await emailInput.count() > 0) { - const invalidEmail = TestDataGenerator.generateInvalidEmail(); - await emailInput.first().fill(invalidEmail); - - await page.keyboard.press('Tab'); - await page.waitForTimeout(300); - - const isValid = await emailInput.first().evaluate((el: HTMLInputElement) => { - return el.checkValidity(); - }); - - expect(isValid).toBe(false); - } - }); - - test('电话格式验证正常', async ({ page }) => { - const phoneInput = page.locator('input[name="phone"], input[placeholder*="电话"]'); - - if (await phoneInput.count() > 0) { - const invalidPhone = TestDataGenerator.generateInvalidPhone(); - await phoneInput.first().fill(invalidPhone); - - await page.keyboard.press('Tab'); - await page.waitForTimeout(300); - - const value = await phoneInput.first().inputValue(); - expect(value.length).toBeLessThan(15); - } - }); - - test('错误提示显示正确', async ({ page }) => { - const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"]'); - - if (await nameInput.count() > 0) { - await nameInput.first().fill(''); - await nameInput.first().blur(); - - await page.waitForTimeout(300); - - const parent = nameInput.first().locator('xpath=..'); - const hasError = await parent.evaluate((el) => { - return el.classList.contains('error') || - el.classList.contains('invalid') || - el.getAttribute('aria-invalid') === 'true'; - }); - - expect(typeof hasError).toBe('boolean'); - } - }); - - test('错误提示可读性良好', async ({ page }) => { - const errorMessages = page.locator('[class*="error"], [class*="invalid"], [role="alert"]'); - - if (await errorMessages.count() > 0) { - const firstError = errorMessages.first(); - const fontSize = await firstError.evaluate((el) => { - const style = window.getComputedStyle(el); - return parseFloat(style.fontSize); - }); - - expect(fontSize).toBeGreaterThanOrEqual(12); - - const color = await firstError.evaluate((el) => { - const style = window.getComputedStyle(el); - return style.color; - }); - - expect(color).toBeTruthy(); - } - }); - }); - - test.describe('提交流程测试', () => { - test('表单提交功能正常', async ({ page }) => { - const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"]'); - const phoneInput = page.locator('input[name="phone"], input[placeholder*="电话"]'); - const emailInput = page.locator('input[name="email"], input[placeholder*="邮箱"]'); - const messageInput = page.locator('textarea[name="message"], textarea[placeholder*="消息"]'); - const submitButton = page.locator('button[type="submit"], button:has-text("提交")'); - - if (await nameInput.count() > 0) { - await nameInput.first().fill(TestDataGenerator.generateName()); - } - - if (await phoneInput.count() > 0) { - await phoneInput.first().fill(TestDataGenerator.generatePhone()); - } - - if (await emailInput.count() > 0) { - await emailInput.first().fill(TestDataGenerator.generateEmail()); - } - - if (await messageInput.count() > 0) { - await messageInput.first().fill(TestDataGenerator.generateMessage()); - } - - if (await submitButton.count() > 0) { - await submitButton.first().click(); - await page.waitForTimeout(2000); - - const successMessage = page.locator('[class*="success"], text=/成功|感谢|已发送/'); - const hasSuccess = await successMessage.count() > 0; - - expect(typeof hasSuccess).toBe('boolean'); - } - }); - - test('成功消息显示正确', async ({ page }) => { - const successMessage = page.locator('[class*="success"], text=/成功|感谢|已发送/'); - - if (await successMessage.count() > 0) { - await expect(successMessage.first()).toBeVisible({ timeout: 5000 }); - - const text = await successMessage.first().textContent(); - expect(text).toBeTruthy(); - expect(text!.length).toBeGreaterThan(0); - } - }); - - test('错误处理正常', async ({ page }) => { - const submitButton = page.locator('button[type="submit"], button:has-text("提交")'); - - if (await submitButton.count() > 0) { - await submitButton.first().click(); - await page.waitForTimeout(1000); - - const errorMessage = page.locator('[class*="error"], [class*="failed"]'); - const hasError = await errorMessage.count() > 0; - - expect(typeof hasError).toBe('boolean'); - } - }); - - test('提交按钮状态变化正常', async ({ page }) => { - const submitButton = page.locator('button[type="submit"], button:has-text("提交")'); - - if (await submitButton.count() > 0) { - const initialState = await submitButton.first().getAttribute('disabled'); - - const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"]'); - if (await nameInput.count() > 0) { - await nameInput.first().fill(TestDataGenerator.generateName()); - } - - const currentState = await submitButton.first().getAttribute('disabled'); - - expect(typeof initialState).toBe('string' || null); - expect(typeof currentState).toBe('string' || null); - } - }); - }); - - test.describe('联系信息测试', () => { - test('地址可点击性正常', async ({ page }) => { - const addressLinks = page.locator('a[href*="map"], a[href*="baidu.com/map"], a[href*="amap.com"]'); - - if (await addressLinks.count() > 0) { - const firstAddress = addressLinks.first(); - const href = await firstAddress.getAttribute('href'); - expect(href).toBeTruthy(); - expect(href).toMatch(/map|baidu|amap/i); - } - }); - - test('电话可点击性正常', async ({ page }) => { - const phoneLinks = page.locator('a[href^="tel:"]'); - const count = await phoneLinks.count(); - - if (count > 0) { - const firstPhone = phoneLinks.first(); - const href = await firstPhone.getAttribute('href'); - expect(href).toMatch(/^tel:\d+/); - - const box = await firstPhone.boundingBox(); - expect(box).toBeTruthy(); - expect(box!.height).toBeGreaterThanOrEqual(44); - } - }); - - test('邮箱可点击性正常', async ({ page }) => { - const emailLinks = page.locator('a[href^="mailto:"]'); - const count = await emailLinks.count(); - - if (count > 0) { - const firstEmail = emailLinks.first(); - const href = await firstEmail.getAttribute('href'); - expect(href).toMatch(/^mailto:.+@.+/); - - const box = await firstEmail.boundingBox(); - expect(box).toBeTruthy(); - expect(box!.height).toBeGreaterThanOrEqual(44); - } - }); - }); - - test.describe('地图集成测试', () => { - test('地图显示正常', async ({ page }) => { - const mapContainer = page.locator('[class*="map"], iframe[src*="map"], #map'); - - if (await mapContainer.count() > 0) { - await expect(mapContainer.first()).toBeVisible({ timeout: 10000 }); - - const box = await mapContainer.first().boundingBox(); - expect(box).toBeTruthy(); - expect(box!.width).toBeGreaterThan(200); - expect(box!.height).toBeGreaterThan(150); - } - }); - - test('地图交互正常', async ({ page }) => { - const mapIframe = page.locator('iframe[src*="map"]'); - - if (await mapIframe.count() > 0) { - const box = await mapIframe.first().boundingBox(); - expect(box).toBeTruthy(); - - await page.mouse.click(box!.x + box!.width / 2, box!.y + box!.height / 2); - await page.waitForTimeout(500); - } - }); - }); - - test.describe('性能测试', () => { - test('联系页加载性能符合标准', async ({ page }) => { - await performanceMonitor.startMonitoring(); - - const metrics = await performanceMonitor.collectMetrics(); - - expect(metrics.loadTime).toBeLessThan(5000); - expect(metrics.firstContentfulPaint).toBeLessThan(1800); - }); - - test('表单输入响应速度正常', async ({ page }) => { - const nameInput = page.locator('input[name="name"], input[placeholder*="姓名"]'); - - if (await nameInput.count() > 0) { - const startTime = Date.now(); - await nameInput.first().fill(TestDataGenerator.generateName()); - const endTime = Date.now(); - - expect(endTime - startTime).toBeLessThan(1000); - } - }); - }); - - test.describe('可访问性测试', () => { - test('表单标签关联正确', async ({ page }) => { - const labels = page.locator('label'); - const count = await labels.count(); - - if (count > 0) { - const firstLabel = labels.first(); - const forAttr = await firstLabel.getAttribute('for'); - - if (forAttr) { - const input = page.locator(`#${forAttr}`); - await expect(input).toBeVisible(); - } - } - }); - - test('表单字段触摸目标大小符合标准', async ({ page }) => { - const inputs = page.locator('input, textarea, select'); - const count = await inputs.count(); - - if (count > 0) { - const firstInput = inputs.first(); - const box = await firstInput.boundingBox(); - - expect(box).toBeTruthy(); - expect(box!.height).toBeGreaterThanOrEqual(44); - } - }); - - test('提交按钮触摸目标大小符合标准', async ({ page }) => { - const submitButton = page.locator('button[type="submit"], button:has-text("提交")'); - - if (await submitButton.count() > 0) { - const box = await submitButton.first().boundingBox(); - - expect(box).toBeTruthy(); - expect(box!.width).toBeGreaterThanOrEqual(44); - expect(box!.height).toBeGreaterThanOrEqual(44); - } - }); - }); -}); diff --git a/e2e/src/tests/mobile/mobile-functionality.spec.ts b/e2e/src/tests/mobile/mobile-functionality.spec.ts deleted file mode 100644 index 0cc1d8e..0000000 --- a/e2e/src/tests/mobile/mobile-functionality.spec.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { test, expect, Page } from '@playwright/test'; -import { MobilePage } from '../pages/MobilePage'; -import { TestDataGenerator } from '../utils/TestDataGenerator'; -import { PerformanceMonitor } from '../utils/PerformanceMonitor'; -import { MobileHelper } from '../utils/MobileHelper'; -import { getCoreMobileDevices } from '../utils/DeviceMatrix'; - -test.describe('首页移动端功能测试 @mobile', () => { - let mobilePage: MobilePage; - let performanceMonitor: PerformanceMonitor; - let mobileHelper: MobileHelper; - - test.beforeEach(async ({ page }) => { - mobilePage = new MobilePage(page); - performanceMonitor = new PerformanceMonitor(page); - mobileHelper = new MobileHelper(page); - await page.goto('/'); - await page.waitForLoadState('domcontentloaded'); - }); - - test.describe('Hero区域测试', () => { - test('Hero标题在移动端可见且响应式', async ({ page }) => { - const heroTitle = page.locator('h1').first(); - await expect(heroTitle).toBeVisible({ timeout: 10000 }); - - const titleText = await heroTitle.textContent(); - expect(titleText).toBeTruthy(); - expect(titleText!.length).toBeGreaterThan(0); - - const box = await heroTitle.boundingBox(); - expect(box).toBeTruthy(); - expect(box!.width).toBeLessThanOrEqual(400); - }); - - test('Hero描述文本可读', async ({ page }) => { - const heroDescription = page.locator('h1 + p, [class*="hero"] p').first(); - await expect(heroDescription).toBeVisible({ timeout: 10000 }); - - const fontSize = await heroDescription.evaluate((el) => { - const style = window.getComputedStyle(el); - return parseFloat(style.fontSize); - }); - expect(fontSize).toBeGreaterThanOrEqual(14); - }); - - test('CTA按钮触摸目标大小符合标准', async ({ page }) => { - const ctaButtons = page.locator('a[href="#contact"], a[href*="contact"], button:has-text("咨询"), button:has-text("联系")'); - const count = await ctaButtons.count(); - - if (count > 0) { - const firstButton = ctaButtons.first(); - const box = await firstButton.boundingBox(); - - expect(box).toBeTruthy(); - expect(box!.width).toBeGreaterThanOrEqual(44); - expect(box!.height).toBeGreaterThanOrEqual(44); - } - }); - - test('CTA按钮点击响应正常', async ({ page }) => { - const ctaButton = page.locator('a[href="#contact"], a[href*="contact"]').first(); - - if (await ctaButton.isVisible()) { - await ctaButton.click(); - await page.waitForTimeout(500); - - const currentUrl = page.url(); - expect(currentUrl).toContain('contact'); - } - }); - }); - - test.describe('导航测试', () => { - test('移动菜单开关功能正常', async ({ page }) => { - const menuButton = page.locator('button[aria-label="打开菜单"], button[aria-label="关闭菜单"]'); - await expect(menuButton).toBeVisible({ timeout: 10000 }); - - await menuButton.click(); - - const mobileMenu = page.locator('#mobile-menu'); - await expect(mobileMenu).toBeVisible({ timeout: 10000 }); - - const closeButton = page.locator('button[aria-label="关闭菜单"]'); - await closeButton.click(); - - await expect(mobileMenu).not.toBeVisible({ timeout: 10000 }); - }); - - test('导航链接点击正常', async ({ page }) => { - const menuButton = page.locator('button[aria-label="打开菜单"]'); - await menuButton.click(); - - const mobileMenu = page.locator('#mobile-menu'); - await expect(mobileMenu).toBeVisible({ timeout: 10000 }); - - const navLinks = mobileMenu.locator('a'); - const count = await navLinks.count(); - - if (count > 0) { - const firstLink = navLinks.first(); - const linkText = await firstLink.textContent(); - - await firstLink.click(); - await page.waitForTimeout(500); - - const closeButton = page.locator('button[aria-label="关闭菜单"]'); - if (await closeButton.isVisible()) { - await closeButton.click(); - } - } - }); - - test('面包屑导航显示正确', async ({ page }) => { - await page.goto('/about'); - await page.waitForLoadState('domcontentloaded'); - - const breadcrumb = page.locator('nav[aria-label="breadcrumb"]'); - await expect(breadcrumb).toBeVisible({ timeout: 10000 }); - - const breadcrumbLinks = breadcrumb.locator('a'); - const count = await breadcrumbLinks.count(); - expect(count).toBeGreaterThanOrEqual(1); - }); - }); - - test.describe('内容区域测试', () => { - test('服务卡片响应式布局正确', async ({ page }) => { - const servicesSection = page.locator('#services, [id*="service"]'); - - if (await servicesSection.count() > 0) { - const serviceCards = servicesSection.first().locator('article, [class*="card"]'); - const count = await serviceCards.count(); - - if (count >= 2) { - const firstCard = serviceCards.first(); - const secondCard = serviceCards.nth(1); - - const firstBox = await firstCard.boundingBox(); - const secondBox = await secondCard.boundingBox(); - - if (firstBox && secondBox) { - expect(secondBox.y).toBeGreaterThan(firstBox.y); - expect(firstBox.width).toBeLessThanOrEqual(400); - } - } - } - }); - - test('产品卡片堆叠布局正确', async ({ page }) => { - const productsSection = page.locator('#products, [id*="product"]'); - - if (await productsSection.count() > 0) { - const productCards = productsSection.first().locator('article, [class*="card"], a'); - const count = await productCards.count(); - - if (count >= 2) { - const firstCard = productCards.first(); - const secondCard = productCards.nth(1); - - const firstBox = await firstCard.boundingBox(); - const secondBox = await secondCard.boundingBox(); - - if (firstBox && secondBox) { - expect(secondBox.y).toBeGreaterThan(firstBox.y); - } - } - } - }); - - test('案例卡片响应式布局正确', async ({ page }) => { - const casesSection = page.locator('#cases, [id*="case"]'); - - if (await casesSection.count() > 0) { - const caseCards = casesSection.first().locator('article, [class*="card"]'); - const count = await caseCards.count(); - expect(count).toBeGreaterThan(0); - - if (count > 0) { - const firstCard = caseCards.first(); - const box = await firstCard.boundingBox(); - - if (box) { - expect(box.width).toBeLessThanOrEqual(400); - } - } - } - }); - - test('新闻卡片响应式布局正确', async ({ page }) => { - const newsSection = page.locator('#news, [id*="news"]'); - - if (await newsSection.count() > 0) { - const newsCards = newsSection.first().locator('article, [class*="card"]'); - const count = await newsCards.count(); - expect(count).toBeGreaterThan(0); - } - }); - }); - - test.describe('页脚测试', () => { - test('联系信息可点击', async ({ page }) => { - const footer = page.locator('footer'); - await expect(footer).toBeVisible({ timeout: 10000 }); - - const phoneLinks = footer.locator('a[href^="tel:"]'); - const phoneCount = await phoneLinks.count(); - - if (phoneCount > 0) { - const firstPhone = phoneLinks.first(); - const href = await firstPhone.getAttribute('href'); - expect(href).toContain('tel:'); - } - - const emailLinks = footer.locator('a[href^="mailto:"]'); - const emailCount = await emailLinks.count(); - - if (emailCount > 0) { - const firstEmail = emailLinks.first(); - const href = await firstEmail.getAttribute('href'); - expect(href).toContain('mailto:'); - } - }); - - test('社交媒体链接正常', async ({ page }) => { - const footer = page.locator('footer'); - const socialLinks = footer.locator('a[target="_blank"], a[href*="weibo"], a[href*="wechat"], a[href*="linkedin"]'); - const count = await socialLinks.count(); - - if (count > 0) { - const firstSocial = socialLinks.first(); - const href = await firstSocial.getAttribute('href'); - expect(href).toBeTruthy(); - } - }); - - test('版权信息显示正确', async ({ page }) => { - const footer = page.locator('footer'); - const copyright = footer.locator('text=/版权|©|Copyright/i'); - - const footerText = await footer.textContent(); - expect(footerText).toMatch(/版权|©|Copyright|公司/i); - }); - }); - - test.describe('表单交互测试', () => { - test('快速联系表单移动端适配', async ({ page }) => { - const contactSection = page.locator('#contact, [id*="contact"]'); - - if (await contactSection.count() > 0) { - const form = contactSection.first().locator('form'); - - if (await form.count() > 0) { - await expect(form.first()).toBeVisible({ timeout: 10000 }); - } - } - }); - - test('表单字段输入正常', async ({ page }) => { - const contactSection = page.locator('#contact, [id*="contact"]'); - - if (await contactSection.count() > 0) { - const nameInput = contactSection.first().locator('input[name="name"], input[placeholder*="姓名"]'); - - if (await nameInput.count() > 0) { - const testData = TestDataGenerator.generateName(); - await nameInput.first().fill(testData); - - const value = await nameInput.first().inputValue(); - expect(value).toBe(testData); - } - } - }); - - test('表单提交功能正常', async ({ page }) => { - const contactSection = page.locator('#contact, [id*="contact"]'); - - if (await contactSection.count() > 0) { - const form = contactSection.first().locator('form'); - - if (await form.count() > 0) { - const nameInput = form.first().locator('input[name="name"], input[placeholder*="姓名"]'); - const phoneInput = form.first().locator('input[name="phone"], input[placeholder*="电话"]'); - const submitButton = form.first().locator('button[type="submit"], button:has-text("提交")'); - - if (await nameInput.count() > 0 && await phoneInput.count() > 0 && await submitButton.count() > 0) { - await nameInput.first().fill(TestDataGenerator.generateName()); - await phoneInput.first().fill(TestDataGenerator.generatePhone()); - - await submitButton.first().click(); - await page.waitForTimeout(1000); - } - } - } - }); - }); - - test.describe('性能测试', () => { - test('页面加载性能符合标准', async ({ page }) => { - await performanceMonitor.startMonitoring(); - - const metrics = await performanceMonitor.collectMetrics(); - - expect(metrics.loadTime).toBeLessThan(5000); - expect(metrics.firstContentfulPaint).toBeLessThan(1800); - }); - - test('LCP符合Core Web Vitals标准', async ({ page }) => { - const lcp = await mobilePage.measureLCP(); - expect(lcp).toBeLessThan(2500); - }); - - test('CLS符合Core Web Vitals标准', async ({ page }) => { - const cls = await mobilePage.measureCLS(); - expect(cls).toBeLessThan(0.1); - }); - }); - - test.describe('可访问性测试', () => { - test('触摸目标大小符合WCAG标准', async ({ page }) => { - const buttons = page.locator('button, a, input, select, textarea'); - const count = await buttons.count(); - - const touchTargets = await buttons.all(); - let validCount = 0; - - for (const target of touchTargets.slice(0, 20)) { - const isValid = await mobilePage.checkTouchTarget(target); - if (isValid) validCount++; - } - - const passRate = validCount / Math.min(touchTargets.length, 20); - expect(passRate).toBeGreaterThan(0.9); - }); - - test('颜色对比度符合WCAG标准', async ({ page }) => { - const textElements = page.locator('p, h1, h2, h3, h4, h5, h6, span, a, label'); - const count = await textElements.count(); - expect(count).toBeGreaterThan(0); - }); - - test('焦点指示器可见', async ({ page }) => { - const focusableElements = page.locator('button, a, input, select, textarea'); - const count = await focusableElements.count(); - - if (count > 0) { - const firstElement = focusableElements.first(); - await firstElement.focus(); - - const outline = await firstElement.evaluate((el) => { - const style = window.getComputedStyle(el); - return style.outline; - }); - - expect(outline).toBeTruthy(); - } - }); - }); -}); diff --git a/e2e/src/utils/DeviceMatrix.ts b/e2e/src/utils/DeviceMatrix.ts deleted file mode 100644 index 751a25a..0000000 --- a/e2e/src/utils/DeviceMatrix.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { devices, Device } from '@playwright/test'; - -export interface MobileDevice { - name: string; - device: Device; - viewport: { width: number; height: number }; - userAgent?: string; -} - -export interface ResponsiveBreakpoint { - name: string; - width: number; - height: number; - description: string; -} - -export const mobileDevices: Record = { - 'iPhone 12': { - name: 'iPhone 12', - device: devices['iPhone 12'], - viewport: { width: 390, height: 844 }, - }, - 'iPhone 14': { - name: 'iPhone 14', - device: devices['iPhone 14'], - viewport: { width: 390, height: 844 }, - }, - 'Galaxy S21': { - name: 'Galaxy S21', - device: devices['Galaxy S21'], - viewport: { width: 360, height: 800 }, - }, - 'iPad Pro': { - name: 'iPad Pro', - device: devices['iPad Pro'], - viewport: { width: 1024, height: 1366 }, - }, - 'iPad Mini': { - name: 'iPad Mini', - device: devices['iPad Mini'], - viewport: { width: 768, height: 1024 }, - }, -}; - -export const responsiveBreakpoints: ResponsiveBreakpoint[] = [ - { - name: 'iPhone SE', - width: 375, - height: 667, - description: 'Small mobile device', - }, - { - name: 'iPhone 11 Pro Max', - width: 414, - height: 896, - description: 'Large mobile device', - }, - { - name: 'iPad Portrait', - width: 768, - height: 1024, - description: 'Tablet portrait', - }, - { - name: 'iPad Landscape', - width: 1024, - height: 768, - description: 'Tablet landscape', - }, - { - name: 'Small Laptop', - width: 1280, - height: 720, - description: 'Small laptop screen', - }, -]; - -export const coreDevices = ['iPhone 12', 'iPhone 14', 'Galaxy S21', 'iPad Pro', 'iPad Mini']; - -export function getMobileDevice(name: string): MobileDevice | undefined { - return mobileDevices[name]; -} - -export function getViewportSize(deviceName: string): { width: number; height: number } | undefined { - const device = mobileDevices[deviceName]; - return device?.viewport; -} - -export function isMobileDevice(userAgent: string): boolean { - const mobilePatterns = [ - /iPhone/i, - /iPad/i, - /iPod/i, - /Android/i, - /BlackBerry/i, - /Windows Phone/i, - /webOS/i, - /Mobile/i, - ]; - - return mobilePatterns.some(pattern => pattern.test(userAgent)); -} - -export function getDeviceByViewport(width: number, height: number): string | undefined { - for (const breakpoint of responsiveBreakpoints) { - if (width <= breakpoint.width && height <= breakpoint.height) { - return breakpoint.name; - } - } - return undefined; -} - -export function getAllMobileDevices(): MobileDevice[] { - return Object.values(mobileDevices); -} - -export function getCoreMobileDevices(): MobileDevice[] { - return coreDevices.map(name => mobileDevices[name]).filter(Boolean) as MobileDevice[]; -} - -export function getAllResponsiveBreakpoints(): ResponsiveBreakpoint[] { - return [...responsiveBreakpoints]; -} - -export function getBreakpointByWidth(width: number): ResponsiveBreakpoint | undefined { - return responsiveBreakpoints.find(bp => width <= bp.width); -} diff --git a/e2e/src/utils/MobileHelper.ts b/e2e/src/utils/MobileHelper.ts deleted file mode 100644 index 2c55c64..0000000 --- a/e2e/src/utils/MobileHelper.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { Page } from '@playwright/test'; -import { getMobileDevice, isMobileDevice, getDeviceByViewport } from './DeviceMatrix'; - -export interface TouchEvent { - type: 'touchstart' | 'touchmove' | 'touchend'; - touches: Array<{ x: number; y: number }>; - timestamp: number; -} - -export interface NetworkCondition { - offline: boolean; - downloadThroughput: number; - uploadThroughput: number; - latency: number; -} - -export class MobileHelper { - constructor(private page: Page) {} - - async getMobileDevice(name: string) { - return getMobileDevice(name); - } - - async getViewportSize(device: string) { - const device = await this.getMobileDevice(device); - return device?.viewport; - } - - async isMobileDevice(userAgent?: string): Promise { - const ua = userAgent || await this.page.evaluate(() => navigator.userAgent); - return isMobileDevice(ua); - } - - async getCurrentViewport(): Promise<{ width: number; height: number }> { - return await this.page.evaluate(() => { - return { - width: window.innerWidth, - height: window.innerHeight, - }; - }); - } - - async getCurrentDeviceName(): Promise { - const viewport = await this.getCurrentViewport(); - return getDeviceByViewport(viewport.width, viewport.height); - } - - async isPortrait(): Promise { - const viewport = await this.getCurrentViewport(); - return viewport.height > viewport.width; - } - - async isLandscape(): Promise { - const viewport = await this.getCurrentViewport(); - return viewport.width > viewport.height; - } - - async getTouchSupport(): Promise<{ - maxTouchPoints: number; - touchEvent: boolean; - }> { - return await this.page.evaluate(() => { - return { - maxTouchPoints: navigator.maxTouchPoints || 0, - touchEvent: 'ontouchstart' in window, - }; - }); - } - - async generateTouchEvent(type: 'touchstart' | 'touchmove' | 'touchend', touches: Array<{ x: number; y: number }>): Promise { - return { - type, - touches, - timestamp: Date.now(), - }; - } - - async simulateTouch(element: string, x: number, y: number): Promise { - await this.page.locator(element).dispatchEvent('touchstart', { - touches: [{ clientX: x, clientY: y }], - }); - - await this.page.waitForTimeout(50); - - await this.page.locator(element).dispatchEvent('touchend', { - touches: [], - }); - } - - async simulateSwipe(startX: number, startY: number, endX: number, endY: number, duration: number = 500): Promise { - const steps = 10; - const stepDelay = duration / steps; - - for (let i = 0; i <= steps; i++) { - const x = startX + (endX - startX) * (i / steps); - const y = startY + (endY - startY) * (i / steps); - - await this.page.mouse.move(x, y); - await this.page.waitForTimeout(stepDelay); - } - } - - async simulatePinchZoom(centerX: number, centerY: number, scale: number): Promise { - const startDistance = 100; - const endDistance = startDistance / scale; - - const finger1Start = { x: centerX - startDistance / 2, y: centerY }; - const finger2Start = { x: centerX + startDistance / 2, y: centerY }; - - const finger1End = { x: centerX - endDistance / 2, y: centerY }; - const finger2End = { x: centerX + endDistance / 2, y: centerY }; - - await this.page.mouse.move(finger1Start.x, finger1Start.y); - await this.page.mouse.down(); - - await this.page.mouse.move(finger2Start.x, finger2Start.y); - await this.page.mouse.down(); - - await this.page.mouse.move(finger1End.x, finger1End.y); - await this.page.mouse.move(finger2End.x, finger2End.y); - - await this.page.mouse.up(); - await this.page.mouse.up(); - } - - async setNetworkCondition(condition: NetworkCondition): Promise { - await this.page.route('**', (route) => { - if (condition.offline) { - route.abort(); - } else { - route.continue(); - } - }); - - await this.page.evaluate((condition) => { - Object.defineProperty(navigator, 'connection', { - value: { - effectiveType: condition.downloadThroughput < 1.5 ? 'slow-2g' : '4g', - downlink: condition.downloadThroughput, - rtt: condition.latency, - }, - writable: true, - }); - }, condition); - } - - async getNetworkCondition(): Promise { - return await this.page.evaluate(() => { - const connection = (navigator as any).connection; - return { - offline: !navigator.onLine, - downloadThroughput: connection?.downlink || 10, - uploadThroughput: connection?.downlink || 10, - latency: connection?.rtt || 100, - }; - }); - } - - async setViewport(width: number, height: number): Promise { - await this.page.setViewportSize({ width, height }); - } - - async setDevice(deviceName: string): Promise { - const device = await this.getMobileDevice(deviceName); - if (device) { - await this.page.setViewportSize(device.viewport); - - if (device.userAgent) { - await this.page.setExtraHTTPHeaders({ - 'User-Agent': device.userAgent, - }); - } - } - } - - async emulateMobile(deviceName: string): Promise { - await this.setDevice(deviceName); - - await this.page.emulateMedia({ - media: 'screen', - colorScheme: 'light', - }); - } - - async getBatteryInfo(): Promise<{ - level: number; - charging: boolean; - }> { - return await this.page.evaluate(() => { - const battery = (navigator as any).getBattery(); - if (battery) { - return { - level: battery.level, - charging: battery.charging, - }; - } - return { - level: 1, - charging: true, - }; - }); - } - - async getOrientation(): Promise<'portrait' | 'landscape'> { - const viewport = await this.getCurrentViewport(); - return viewport.height > viewport.width ? 'portrait' : 'landscape'; - } - - async rotateDevice(): Promise { - const viewport = await this.getCurrentViewport(); - await this.page.setViewportSize({ - width: viewport.height, - height: viewport.width, - }); - } - - async hideKeyboard(): Promise { - await this.page.keyboard.press('Escape'); - await this.page.waitForTimeout(200); - } - - async scrollToElement(selector: string): Promise { - const element = this.page.locator(selector); - await element.scrollIntoViewIfNeeded(); - await this.page.waitForTimeout(500); - } - - async scrollToTop(): Promise { - await this.page.evaluate(() => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - }); - await this.page.waitForTimeout(500); - } - - async scrollToBottom(): Promise { - await this.page.evaluate(() => { - window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); - }); - await this.page.waitForTimeout(500); - } - - async getScrollPosition(): Promise<{ x: number; y: number }> { - return await this.page.evaluate(() => { - return { - x: window.scrollX, - y: window.scrollY, - }; - }); - } - - async isElementInViewport(selector: string): Promise { - return await this.page.locator(selector).evaluate((el) => { - const rect = el.getBoundingClientRect(); - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && - rect.right <= (window.innerWidth || document.documentElement.clientWidth) - ); - }); - } - - async waitForElementVisible(selector: string, timeout: number = 5000): Promise { - await this.page.waitForSelector(selector, { state: 'visible', timeout }); - } - - async waitForElementHidden(selector: string, timeout: number = 5000): Promise { - await this.page.waitForSelector(selector, { state: 'hidden', timeout }); - } -} diff --git a/e2e/src/utils/PerformanceMonitor.ts b/e2e/src/utils/PerformanceMonitor.ts index 5e0bc9b..2cdf0e1 100644 --- a/e2e/src/utils/PerformanceMonitor.ts +++ b/e2e/src/utils/PerformanceMonitor.ts @@ -314,201 +314,4 @@ export class PerformanceMonitor { return report; } - - async measureFirstMeaningfulPaint(): Promise { - const fmp = 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].startTime); - } - }); - observer.observe({ entryTypes: ['first-meaningful-paint'] }); - setTimeout(() => resolve(0), 5000); - } else { - resolve(0); - } - }); - }); - return fmp; - } - - async measureNetworkPerformance(): Promise<{ - dnsLookup: number; - tcpConnection: number; - sslHandshake: number; - requestTime: number; - responseTime: number; - totalTime: number; - }> { - const timing = await this.page.evaluate(() => { - const perf = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; - return { - dnsLookup: perf.domainLookupEnd - perf.domainLookupStart, - tcpConnection: perf.connectEnd - perf.connectStart, - sslHandshake: perf.secureConnectionStart > 0 ? perf.connectEnd - perf.secureConnectionStart : 0, - requestTime: perf.responseStart - perf.requestStart, - responseTime: perf.responseEnd - perf.responseStart, - totalTime: perf.loadEventEnd - perf.fetchStart, - }; - }); - return timing; - } - - async measureBatteryImpact(): Promise<{ - estimatedImpact: string; - recommendations: string[]; - }> { - const metrics = await this.collectMetrics(); - const recommendations: string[] = []; - let impact = 'low'; - - if (metrics.loadTime > 3000) { - recommendations.push('页面加载时间过长,建议优化资源加载'); - impact = 'high'; - } - if (metrics.firstInputDelay > 100) { - recommendations.push('首次输入延迟较高,建议优化JavaScript执行'); - impact = impact === 'high' ? 'high' : 'medium'; - } - if (metrics.largestContentfulPaint > 2500) { - recommendations.push('最大内容绘制时间过长,建议优化关键渲染路径'); - impact = impact === 'high' ? 'high' : 'medium'; - } - - return { - estimatedImpact: impact, - recommendations, - }; - } - - async validateLCP(value: number, threshold: number = 2500): boolean { - return value <= threshold; - } - - async validateFID(value: number, threshold: number = 100): boolean { - return value <= threshold; - } - - async validateCLS(value: number, threshold: number = 0.1): boolean { - return value <= threshold; - } - - async validateTTI(value: number, threshold: number = 3500): boolean { - return value <= threshold; - } - - async validateTTFB(value: number, threshold: number = 600): boolean { - return value <= threshold; - } - - async validateFCP(value: number, threshold: number = 1800): boolean { - return value <= threshold; - } - - async getCoreWebVitalsSummary(): Promise<{ - lcp: { value: number; threshold: number; passed: boolean }; - fid: { value: number; threshold: number; passed: boolean }; - cls: { value: number; threshold: number; passed: boolean }; - tti: { value: number; threshold: number; passed: boolean }; - ttfb: { value: number; threshold: number; passed: boolean }; - fcp: { value: number; threshold: number; passed: boolean }; - }> { - const metrics = await this.collectMetrics(); - const ttfb = await this.measureFirstByteTime(); - const fcp = await this.measureFirstContentfulPaint(); - - return { - lcp: { - value: metrics.largestContentfulPaint, - threshold: 2500, - passed: await this.validateLCP(metrics.largestContentfulPaint), - }, - fid: { - value: metrics.firstInputDelay, - threshold: 100, - passed: await this.validateFID(metrics.firstInputDelay), - }, - cls: { - value: metrics.cumulativeLayoutShift, - threshold: 0.1, - passed: await this.validateCLS(metrics.cumulativeLayoutShift), - }, - tti: { - value: metrics.timeToInteractive, - threshold: 3500, - passed: await this.validateTTI(metrics.timeToInteractive), - }, - ttfb: { - value: ttfb, - threshold: 600, - passed: await this.validateTTFB(ttfb), - }, - fcp: { - value: fcp, - threshold: 1800, - passed: await this.validateFCP(fcp), - }, - }; - } - - async measureFirstByteTime(): Promise { - const ttfb = await this.page.evaluate(() => { - const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; - return timing.responseStart - timing.fetchStart; - }); - return ttfb; - } - - async measureMobileSpecificMetrics(): Promise<{ - touchResponseTime: number; - scrollPerformance: number; - gestureLatency: number; - }> { - const touchResponseTime = await this.page.evaluate(() => { - return new Promise((resolve) => { - const startTime = performance.now(); - document.addEventListener('touchstart', () => { - resolve(performance.now() - startTime); - }, { once: true }); - setTimeout(() => resolve(0), 1000); - }); - }); - - const scrollPerformance = await this.page.evaluate(() => { - return new Promise((resolve) => { - const startTime = performance.now(); - let frames = 0; - - function countFrames() { - frames++; - if (performance.now() - startTime >= 1000) { - resolve(frames); - } else { - requestAnimationFrame(countFrames); - } - } - - requestAnimationFrame(countFrames); - }); - }); - - const gestureLatency = await this.page.evaluate(() => { - return new Promise((resolve) => { - const startTime = performance.now(); - document.addEventListener('touchmove', () => { - resolve(performance.now() - startTime); - }, { once: true }); - setTimeout(() => resolve(0), 500); - }); - }); - - return { - touchResponseTime, - scrollPerformance, - gestureLatency, - }; - } } diff --git a/e2e/src/utils/TestDataGenerator.ts b/e2e/src/utils/TestDataGenerator.ts index 64ffbe1..6a472cf 100644 --- a/e2e/src/utils/TestDataGenerator.ts +++ b/e2e/src/utils/TestDataGenerator.ts @@ -457,347 +457,4 @@ ${this.generateMessage()}`; const suffix = Math.floor(Math.random() * 90000000 + 10000000); return `${prefix}${suffix}`; } - - static generateMobileDevice(): { - name: string; - userAgent: string; - viewport: { width: number; height: number }; - devicePixelRatio: number; - touchPoints: number; - } { - const devices = [ - { - name: 'iPhone 12', - userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1', - viewport: { width: 390, height: 844 }, - devicePixelRatio: 3, - touchPoints: 5, - }, - { - name: 'iPhone 14', - userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', - viewport: { width: 390, height: 844 }, - devicePixelRatio: 3, - touchPoints: 5, - }, - { - name: 'Galaxy S21', - userAgent: 'Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36', - viewport: { width: 360, height: 800 }, - devicePixelRatio: 3, - touchPoints: 5, - }, - { - name: 'iPad Pro', - userAgent: 'Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1', - viewport: { width: 1024, height: 1366 }, - devicePixelRatio: 2, - touchPoints: 5, - }, - ]; - return devices[Math.floor(Math.random() * devices.length)]!; - } - - static generateMobileNetworkCondition(): { - offline: boolean; - downloadThroughput: number; - uploadThroughput: number; - latency: number; - name: string; - } { - const conditions = [ - { - offline: false, - downloadThroughput: 10, - uploadThroughput: 5, - latency: 100, - name: '4G', - }, - { - offline: false, - downloadThroughput: 1.5, - uploadThroughput: 0.75, - latency: 300, - name: '3G', - }, - { - offline: false, - downloadThroughput: 0.4, - uploadThroughput: 0.2, - latency: 1000, - name: '2G', - }, - { - offline: true, - downloadThroughput: 0, - uploadThroughput: 0, - latency: 0, - name: 'Offline', - }, - ]; - return conditions[Math.floor(Math.random() * conditions.length)]!; - } - - static generateTouchEvent(): { - type: 'touchstart' | 'touchmove' | 'touchend'; - touches: Array<{ x: number; y: number }>; - timestamp: number; - } { - const types: Array<'touchstart' | 'touchmove' | 'touchend'> = ['touchstart', 'touchmove', 'touchend']; - const type = types[Math.floor(Math.random() * types.length)]!; - const touches = Array.from({ length: Math.floor(Math.random() * 3) + 1 }, () => ({ - x: Math.floor(Math.random() * 400), - y: Math.floor(Math.random() * 800), - })); - - return { - type, - touches, - timestamp: Date.now(), - }; - } - - static generateSwipeGesture(): { - startX: number; - startY: number; - endX: number; - endY: number; - duration: number; - } { - const startX = Math.floor(Math.random() * 300) + 50; - const startY = Math.floor(Math.random() * 700) + 50; - const direction = Math.random() < 0.5 ? 1 : -1; - - return { - startX, - startY, - endX: startX + Math.floor(Math.random() * 200) * direction, - endY: startY + Math.floor(Math.random() * 200) * direction, - duration: Math.floor(Math.random() * 500) + 200, - }; - } - - static generatePinchZoomGesture(): { - centerX: number; - centerY: number; - scale: number; - duration: number; - } { - return { - centerX: Math.floor(Math.random() * 300) + 50, - centerY: Math.floor(Math.random() * 700) + 50, - scale: Math.random() * 2 + 0.5, - duration: Math.floor(Math.random() * 500) + 200, - }; - } - - static generateMobilePerformanceThresholds(): { - lcp: { good: number; needsImprovement: number; poor: number }; - fid: { good: number; needsImprovement: number; poor: number }; - cls: { good: number; needsImprovement: number; poor: number }; - tti: { good: number; needsImprovement: number; poor: number }; - ttfb: { good: number; needsImprovement: number; poor: number }; - fcp: { good: number; needsImprovement: number; poor: number }; - } { - return { - lcp: { good: 2500, needsImprovement: 4000, poor: 4000 }, - fid: { good: 100, needsImprovement: 300, poor: 300 }, - cls: { good: 0.1, needsImprovement: 0.25, poor: 0.25 }, - tti: { good: 3500, needsImprovement: 5000, poor: 5000 }, - ttfb: { good: 600, needsImprovement: 1500, poor: 1500 }, - fcp: { good: 1800, needsImprovement: 3000, poor: 3000 }, - }; - } - - static generateMobileAccessibilityTestData(): { - touchTargetSize: { min: number; recommended: number }; - colorContrast: { normalText: number; largeText: number }; - fontScale: { min: number; max: number }; - spacing: { min: number }; - focusIndicator: { minSize: number }; - } { - return { - touchTargetSize: { min: 44, recommended: 48 }, - colorContrast: { normalText: 4.5, largeText: 3.0 }, - fontScale: { min: 1.0, max: 2.0 }, - spacing: { min: 8 }, - focusIndicator: { minSize: 2 }, - }; - } - - static generateMobileFormTestData(): { - name: string; - email: string; - phone: string; - company: string; - message: string; - subject: string; - touchTargets: Array<{ selector: string; size: { width: number; height: number } }>; - } { - return { - name: this.generateName(), - email: this.generateEmail(), - phone: this.generatePhone(), - company: this.generateCompany(), - message: this.generateMessage(), - subject: this.generateSubject(), - touchTargets: [ - { selector: 'input[name="name"]', size: { width: 350, height: 48 } }, - { selector: 'input[name="email"]', size: { width: 350, height: 48 } }, - { selector: 'input[name="phone"]', size: { width: 350, height: 48 } }, - { selector: 'textarea[name="message"]', size: { width: 350, height: 150 } }, - { selector: 'button[type="submit"]', size: { width: 350, height: 48 } }, - ], - }; - } - - static generateMobileNavigationTestData(): { - menuItems: Array<{ label: string; href: string; touchTarget: boolean }>; - hamburgerMenu: { selector: string; size: { width: number; height: number } }; - breadcrumbs: Array<{ label: string; href: string }>; - } { - return { - menuItems: [ - { label: '首页', href: '#home', touchTarget: true }, - { label: '关于我们', href: '#about', touchTarget: true }, - { label: '服务', href: '#services', touchTarget: true }, - { label: '产品', href: '#products', touchTarget: true }, - { label: '案例', href: '#cases', touchTarget: true }, - { label: '新闻', href: '#news', touchTarget: true }, - { label: '联系我们', href: '#contact', touchTarget: true }, - ], - hamburgerMenu: { - selector: 'button[aria-label="打开菜单"]', - size: { width: 48, height: 48 }, - }, - breadcrumbs: [ - { label: '首页', href: '/' }, - { label: '服务', href: '#services' }, - { label: '详情', href: '#details' }, - ], - }; - } - - static generateMobileScrollTestData(): { - scrollPositions: Array<{ x: number; y: number }>; - scrollSpeeds: Array<{ pixelsPerSecond: number }>; - scrollDirections: Array<'up' | 'down' | 'left' | 'right'>; - } { - return { - scrollPositions: [ - { x: 0, y: 0 }, - { x: 0, y: 500 }, - { x: 0, y: 1000 }, - { x: 0, y: 1500 }, - { x: 0, y: 2000 }, - ], - scrollSpeeds: [ - { pixelsPerSecond: 500 }, - { pixelsPerSecond: 1000 }, - { pixelsPerSecond: 1500 }, - { pixelsPerSecond: 2000 }, - ], - scrollDirections: ['up', 'down', 'left', 'right'], - }; - } - - static generateMobileOrientationTestData(): { - portrait: { width: number; height: number }; - landscape: { width: number; height: number }; - rotation: { from: string; to: string }[]; - } { - return { - portrait: { width: 390, height: 844 }, - landscape: { width: 844, height: 390 }, - rotation: [ - { from: 'portrait', to: 'landscape' }, - { from: 'landscape', to: 'portrait' }, - ], - }; - } - - static generateMobileBatteryTestData(): { - levels: Array<{ level: number; charging: boolean }>; - lowBatteryThreshold: number; - criticalBatteryThreshold: number; - } { - return { - levels: [ - { level: 1.0, charging: true }, - { level: 0.75, charging: false }, - { level: 0.5, charging: false }, - { level: 0.25, charging: false }, - { level: 0.1, charging: false }, - ], - lowBatteryThreshold: 0.2, - criticalBatteryThreshold: 0.1, - }; - } - - static generateMobileGestureTestData(): { - tap: { duration: number; coordinates: Array<{ x: number; y: number }> }; - doubleTap: { interval: number; coordinates: Array<{ x: number; y: number }> }; - longPress: { duration: number; coordinates: Array<{ x: number; y: number }> }; - swipe: { directions: Array<'left' | 'right' | 'up' | 'down'>; distance: number }; - pinch: { scales: Array; duration: number }; - } { - return { - tap: { - duration: 100, - coordinates: [ - { x: 200, y: 400 }, - { x: 300, y: 500 }, - { x: 150, y: 600 }, - ], - }, - doubleTap: { - interval: 300, - coordinates: [ - { x: 200, y: 400 }, - { x: 300, y: 500 }, - ], - }, - longPress: { - duration: 1000, - coordinates: [ - { x: 200, y: 400 }, - { x: 300, y: 500 }, - ], - }, - swipe: { - directions: ['left', 'right', 'up', 'down'], - distance: 200, - }, - pinch: { - scales: [0.5, 1.5, 2.0], - duration: 500, - }, - }; - } - - static generateMobileErrorScenarios(): { - networkError: { message: string; selector: string }; - timeoutError: { message: string; timeout: number }; - touchError: { message: string; selector: string }; - viewportError: { message: string; viewport: { width: number; height: number } }; - } { - return { - networkError: { - message: '网络连接失败,请检查您的网络设置', - selector: '.network-error', - }, - timeoutError: { - message: '请求超时,请稍后重试', - timeout: 30000, - }, - touchError: { - message: '触摸目标不可用', - selector: '.touch-error', - }, - viewportError: { - message: '当前视口大小不支持', - viewport: { width: 320, height: 480 }, - }, - }; - } } diff --git a/src/app/globals.css b/src/app/globals.css index 324b78c..1b80da7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -38,9 +38,9 @@ /* 文字色系 - 墨色层次 */ --color-text-primary: #1C1C1C; - --color-text-secondary: #2D2D2D; - --color-text-tertiary: #4A4A4A; - --color-text-muted: #6B6B6B; + --color-text-secondary: #3D3D3D; + --color-text-tertiary: #5C5C5C; + --color-text-muted: #8C8C8C; /* 边框色系 */ --color-border-primary: #E5E5E5; @@ -211,38 +211,12 @@ input:focus, textarea:focus { outline: none; - box-shadow: 0 0 0 3px rgba(196, 30, 58, 0.3); - } - - button:focus-visible, a:focus-visible { - outline: 2px solid #C41E3A; - outline-offset: 2px; } ::selection { background-color: var(--color-text-primary); color: var(--color-bg-primary); } - - @media (prefers-contrast: high) { - :root { - --color-text-primary: #000000; - --color-text-secondary: #1A1A1A; - --color-text-tertiary: #2A2A2A; - --color-border-primary: #000000; - --color-border-secondary: #1A1A1A; - } - } - - @media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } - } } @layer utilities { diff --git a/src/components/layout/breadcrumb.tsx b/src/components/layout/breadcrumb.tsx index 48361b1..567a607 100644 --- a/src/components/layout/breadcrumb.tsx +++ b/src/components/layout/breadcrumb.tsx @@ -1,7 +1,7 @@ 'use client'; import Link from 'next/link'; -import { ChevronRight, Home, ArrowLeft } from 'lucide-react'; +import { ChevronRight, Home } from 'lucide-react'; interface BreadcrumbItem { label: string; @@ -10,31 +10,20 @@ interface BreadcrumbItem { interface BreadcrumbProps { items: BreadcrumbItem[]; - showBackButton?: boolean; - onBackClick?: () => void; } -export function Breadcrumb({ items, showBackButton = false, onBackClick }: BreadcrumbProps) { +export function Breadcrumb({ items }: BreadcrumbProps) { return ( -