Commit e10fff24 authored by Cai Wei's avatar Cai Wei

feat(*): 完善测试流程

parent 8b7e11c6
Pipeline #1312 failed
# DC-TOM API接口测试集成说明
## 📋 概述
已成功在本地CI脚本中集成了首页API接口正确性测试,现在支持独立运行API接口测试并在测试报告中展示结果。
## 🎯 API测试覆盖范围
### 测试的接口
- **健康度概览接口** (`/api/home/health/overview`)
- **健康度统计接口** (`/api/home/health/statistic`)
- **异常监控接口** (`/api/home/exception/monitor`)
- **除尘器告警接口** (`/api/home/duster/alarm`)
### 测试类型
- ✅ 基础功能测试(状态码、响应结构、数据类型)
- ✅ 错误处理测试(服务器错误、认证错误、网络错误)
- ✅ 性能测试(响应时间验证)
- ✅ 并发测试(同时访问多个接口)
- ✅ 数据一致性测试(多次调用数据一致性)
## 🚀 使用方法
### 1. 独立运行API测试
```bash
# 只运行API接口测试
./local-ci.sh api
# 或使用npm脚本
npm run ci:api
```
### 2. 作为完整测试的一部分
```bash
# 运行所有测试(包含API测试)
./local-ci.sh all
# 或
./local-ci.sh
```
### 3. 直接运行Cypress API测试
```bash
# 使用Cypress直接运行
npx cypress run --spec "cypress/e2e/dashboard-api.cy.js"
# 或使用npm脚本
npm run cy:run:api
```
## 📊 测试报告
### 报告结构
API测试结果会包含在以下报告中:
1. **完整测试报告** (`public/reports/merged-report.html`)
- 包含所有测试类型的汇总结果
2. **API专项报告** (`public/reports/api/`)
- 专门的API接口测试报告
- 详细展示每个接口的测试结果
3. **分类报告入口** (`public/index.html`)
- 提供各类测试报告的快速入口
- 包含API测试专项链接
### 报告访问
测试完成后,报告会自动在浏览器中打开,您可以:
- 点击"🔗 API接口测试"查看专项API测试结果
- 点击"📊 完整测试报告"查看所有测试的汇总
## 🔧 技术实现
### 文件结构
```
cypress/e2e/
├── dashboard-api.cy.js # API接口测试脚本
├── login.cy.js # 基础测试
├── dashboard.cy.js # 基础测试
└── ...
cypress/reports/
├── api/ # API测试报告目录
├── basic/ # 基础测试报告目录
├── business/ # 业务测试报告目录
└── data/ # 数据测试报告目录
public/
├── reports/
│ ├── merged-report.html # 汇总报告
│ ├── api/ # API专项报告
│ └── ...
└── index.html # 报告入口页面
```
### 脚本集成
- **local-ci.sh**: 添加了`api_tests()`函数和`api`测试类型
- **package.json**: 添加了`cy:run:api``ci:api`脚本命令
## 🎯 测试验证
API测试会验证以下内容:
### 响应验证
- HTTP状态码正确性
- 响应体结构完整性
- 数据类型正确性
- 业务数据合理性(如健康度0-100%)
### 错误处理
- 服务器错误响应处理
- 认证失败处理
- 网络错误处理
- 超时情况处理
### 性能监控
- 接口响应时间(< 5秒)
- 并发访问性能
- 数据一致性检查
## 📝 使用示例
```bash
# 1. 快速运行API测试
npm run ci:api
# 2. 运行完整测试套件
npm run ci:local
# 3. 查看测试帮助
./local-ci.sh help
# 4. 运行特定类型测试
./local-ci.sh basic # 基础功能测试
./local-ci.sh api # API接口测试
./local-ci.sh business # 业务功能测试
./local-ci.sh data # 数据管理测试
```
## 🔍 故障排除
如果API测试失败,请检查:
1. 应用服务器是否正常启动(端口3000)
2. 接口是否正常响应
3. 认证TOKEN是否有效
4. 网络连接是否正常
测试报告中会详细记录失败原因和截图,便于问题定位。
\ No newline at end of file
# Cypress 测试启动指导
## 问题解决
你遇到的 `cy.visit() failed trying to load: http://localhost:5173/login` 错误已经修复。主要修复包括:
### 1. 路由修复
项目使用 Hash 路由模式,正确的 URL 格式应该是:
-`/#/login` (正确)
-`/login` (错误)
### 2. 权限控制处理
项目有路由守卫,需要 localStorage 中有 `menuList` 才能访问非登录页面。现在测试中使用 `cy.mockLogin()` 来模拟登录状态。
## 启动步骤
### 1. 启动开发服务器
```bash
npm run dev
```
确保服务器在 `http://localhost:5173` 启动
### 2. 运行 Cypress 测试
**选项A: 图形界面模式**
```bash
npm run cy:open
```
**选项B: 无头模式运行**
```bash
npm run cy:run
```
### 3. 验证修复
现在运行登录测试应该能够:
1. 正确访问 `/#/login` 页面
2. 显示登录表单元素
3. 处理路由守卫逻辑
## 测试覆盖
修复后的测试套件包括:
### 📋 登录功能 (`login.cy.js`)
- ✅ 登录页面元素显示
- ✅ 表单验证
- ✅ 密码可见性切换
- ✅ 验证码处理
- ✅ 路由守卫测试
### 📊 仪表板功能 (`dashboard.cy.js`)
- ✅ 核心组件渲染
- ✅ 健康度指标
- ✅ 图表和地图组件
- ✅ 响应式布局
### 📋 除尘器概览 (`dust-overview.cy.js`)
- ✅ 数据表格
- ✅ 搜索筛选
- ✅ 操作按钮
- ✅ 分页功能
### 🧭 导航菜单 (`navigation.cy.js`)
- ✅ 菜单显示
- ✅ 路由跳转
- ✅ 权限控制
## 注意事项
1. **确保开发服务器已启动**: 测试前必须先运行 `npm run dev`
2. **Hash 路由**: 所有 URL 都需要包含 `#` 符号
3. **权限模拟**: 使用 `cy.mockLogin()` 设置必要的 localStorage 数据
4. **网络请求**: 测试可能会产生实际的 API 请求,考虑使用 cy.intercept() 进行模拟
现在你可以重新运行 Cypress 测试,登录功能测试应该能够正常工作了!
\ No newline at end of file
# Cypress UI 自动化测试文档
## 概述
本项目使用 Cypress 进行 E2E (端到端) UI 自动化测试,覆盖了 DCTOM 除尘器智能管控平台的核心功能。
## 测试架构
### 技术栈
- **测试框架**: Cypress 13.6.0
- **前端框架**: Vue 3 + Element Plus
- **构建工具**: Vite
- **CI/CD**: GitHub Actions
### 测试标识符策略
所有可测试的 UI 元素都使用 `data-testid` 属性进行标识,遵循以下命名规范:
- 页面容器: `{page-name}-container`
- 表单输入: `{form-name}-{field-name}-input`
- 按钮: `{action-name}-button`
- 表格: `{table-name}-table`
- 卡片组件: `{card-name}-card`
### 目录结构
```
cypress/
├── e2e/ # E2E 测试用例
│ ├── login.cy.js # 登录功能测试
│ ├── dashboard.cy.js # 仪表板功能测试
│ ├── dust-overview.cy.js # 除尘器概览测试
│ └── navigation.cy.js # 导航菜单测试
├── fixtures/ # 测试数据
│ └── testData.json # 静态测试数据
├── support/ # 支持文件
│ ├── commands.js # 自定义命令
│ ├── e2e.js # E2E 配置
│ └── component.js # 组件测试配置
└── screenshots/ # 失败时的截图
└── videos/ # 测试录制视频
```
## 核心测试用例
### 1. 登录功能测试 (login.cy.js)
**测试范围:**
- 登录表单元素显示
- 表单验证
- 密码可见性切换
- 验证码刷新
- 记住密码功能
- 键盘导航
- 响应式设计
**关键测试点:**
- 用户名/密码输入验证
- 验证码图片加载和刷新
- 登录状态处理
### 2. 仪表板功能测试 (dashboard.cy.js)
**测试范围:**
- 核心组件渲染
- 健康度指标显示
- 图表数据加载
- 地图组件交互
- 数据更新响应
**关键测试点:**
- 健康度数值格式验证
- 进度条颜色状态
- 图表内容检查
- 响应式布局
### 3. 除尘器概览测试 (dust-overview.cy.js)
**测试范围:**
- 统计卡片显示
- 搜索和筛选功能
- 数据表格操作
- 分页功能
- 新增/编辑操作
**关键测试点:**
- 搜索功能验证
- 表格数据加载
- 操作按钮交互
- 弹窗处理
### 4. 导航菜单测试 (navigation.cy.js)
**测试范围:**
- 菜单项显示
- 导航跳转
- 子菜单展开/收起
- 活跃状态高亮
- 移动端适配
**关键测试点:**
- 菜单权限控制
- 路由跳转验证
- 菜单状态管理
## 自定义命令
项目提供了以下自定义 Cypress 命令:
### 登录相关
```javascript
cy.loginWithUI(username, password) // UI 登录
```
### 导航相关
```javascript
cy.navigateToPage(pageName) // 页面导航
```
### 表格相关
```javascript
cy.checkTableDataLoaded(selector) // 检查表格数据加载
cy.searchInTable(searchText) // 表格搜索
```
### 响应式测试
```javascript
cy.checkResponsive() // 检查响应式设计
```
### 健康度验证
```javascript
cy.verifyHealthIndicators() // 验证健康度指标
```
## 运行测试
### 本地开发
```bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 打开 Cypress 测试界面
npm run cy:open
# 无头模式运行所有测试
npm run cy:run
```
### CI/CD 环境
测试会在以下情况自动触发:
- 推送到 master 或 develop 分支
- 创建 Pull Request 到 master 分支
```bash
# CI 环境运行命令
npm run cy:run:ci
```
## 配置说明
### Cypress 配置 (cypress.config.js)
```javascript
{
baseUrl: 'http://localhost:5173', // 应用基础URL
viewportWidth: 1280, // 默认视口宽度
viewportHeight: 720, // 默认视口高度
defaultCommandTimeout: 10000, // 命令超时时间
video: true, // 录制视频
screenshotOnRunFailure: true // 失败时截图
}
```
### 环境变量
```javascript
env: {
apiUrl: 'http://localhost:3000', // API 地址
username: 'test@example.com', // 测试用户名
password: 'testpassword' // 测试密码
}
```
## 最佳实践
### 1. 测试标识符使用
- 优先使用 `data-testid` 属性
- 避免依赖 CSS 类名或 ID
- 保持标识符语义化和一致性
### 2. 等待策略
```javascript
// ✅ 好的做法
cy.get('[data-testid="submit-button"]').should('be.visible')
// ❌ 避免硬编码等待
cy.wait(5000)
```
### 3. 测试数据管理
- 使用 fixtures 管理静态测试数据
- 在测试前重置数据状态
- 避免测试间的数据依赖
### 4. 断言策略
```javascript
// 检查元素存在性
cy.get('[data-testid="element"]').should('exist')
// 检查元素可见性
cy.get('[data-testid="element"]').should('be.visible')
// 检查元素内容
cy.get('[data-testid="element"]').should('contain', 'Expected Text')
```
## 报告和分析
### 测试结果
- 测试通过/失败统计
- 执行时间分析
- 错误截图和视频录制
### 性能监控
- 集成 Lighthouse CI 进行性能测试
- 监控页面加载时间
- 检查可访问性和 SEO 指标
### CI/CD 集成
- GitHub Actions 自动运行测试
- 测试结果通知
- 失败时的调试信息收集
## 故障排除
### 常见问题
1. **元素未找到**
- 检查 `data-testid` 是否正确
- 确认元素是否在 DOM 中渲染
- 添加适当的等待条件
2. **测试超时**
- 增加命令超时时间
- 优化等待策略
- 检查网络请求响应时间
3. **跨浏览器兼容性**
- 在多个浏览器中测试
- 检查浏览器特定的行为差异
### 调试技巧
```javascript
// 添加调试断点
cy.debug()
// 暂停测试执行
cy.pause()
// 在浏览器控制台输出
cy.window().then(console.log)
```
## 扩展计划
1. **组件测试**: 添加 Vue 组件的单元测试
2. **API 测试**: 集成 API 接口测试
3. **视觉回归测试**: 添加 UI 视觉对比测试
4. **性能测试**: 扩展性能监控覆盖范围
5. **移动端测试**: 增加移动设备测试场景
## 维护指南
### 定期维护任务
- 更新测试数据
- 清理过时的测试用例
- 优化测试执行时间
- 更新文档和最佳实践
### 团队协作
- 代码审查测试用例
- 分享测试经验和技巧
- 建立测试用例编写标准
- 定期评估测试覆盖率
\ No newline at end of file
# GitLab 12.0.3 兼容性配置说明
## 版本信息
- **GitLab版本**: 12.0.3 (2019年发布)
- **项目**: DC-TOM dctomproject
- **分支**: projects
## 兼容性调整
### 1. 语法替换
#### 使用 `only` 替代 `rules`
GitLab 12.0.3 不支持现代的 `rules` 语法,需要使用传统的 `only/except` 语法:
```yaml
# ✅ GitLab 12.0.3 兼容语法
only:
- web # 手动触发
- projects # 分支触发
- merge_requests # MR触发
# ❌ 不兼容语法 (GitLab 12.8+)
rules:
- if: $CI_PIPELINE_SOURCE == "web"
```
#### 使用 `dependencies` 替代 `needs`
`needs` 关键字在 GitLab 12.3 之后才引入:
```yaml
# ✅ GitLab 12.0.3 兼容语法
dependencies:
- build
# ❌ 不兼容语法 (GitLab 12.3+)
needs: ["build"]
```
### 2. Docker镜像优化
使用更稳定且兼容的Docker镜像版本:
```yaml
# ✅ 稳定兼容的镜像
image: cypress/browsers:node16.14.2-slim-chrome103-ff102
image: node:16-alpine
# 原配置使用更新的镜像可能有兼容性问题
```
### 3. 功能配置
#### 触发方式
- **自动触发**: 推送到 `projects` 分支、创建MR
- **手动触发**: 通过Web界面手动运行pipeline
#### 测试策略保持不变
- ✅ 基础测试 (login, dashboard, navigation)
- ✅ 完整测试 (所有测试用例) - 手动触发
- ✅ 并行测试 (业务功能组 + 数据管理组) - 手动触发
#### 报告生成
- ✅ 使用 mochawesome 生成HTML报告
- ✅ 自动部署到GitLab Pages
- ✅ 保存截图和视频用于调试
## 使用方法
### 1. 提交代码
```bash
git add .gitlab-ci.yml
git commit -m "feat: GitLab 12.0.3兼容的CI配置"
git push origin projects
```
### 2. 手动触发测试
1. **访问GitLab项目页面**:
```
https://app.bmetech.com/liuzhaohui/dctomproject
```
2. **进入CI/CD → Pipelines**
3. **点击"Run Pipeline"**
4. **选择运行的测试**:
- 默认运行:build + cypress-basic-tests
- 手动运行:cypress-full-tests (完整测试)
- 手动运行:cypress-business-tests (业务测试)
- 手动运行:cypress-data-tests (数据测试)
### 3. 查看结果
- **Pipeline状态**: 在Pipelines页面查看
- **测试报告**: https://liuzhaohui.gitlab.io/dctomproject/
- **下载Artifacts**: 包含截图、视频、详细报告
## GitLab 12.0.3 特殊注意事项
### 限制和特性
1. **需要手动管理依赖关系**
- `dependencies` 字段需要明确列出前置任务
- 不支持并行依赖的自动解析
2. **Pipeline可视化有限**
- 依赖关系图显示相对简单
- 需要通过任务执行时间推断并行度
3. **缓存功能相对基础**
- 支持基本的文件缓存
- 缓存键的表达式功能有限
4. **错误处理相对简单**
- 重试机制需要手动实现
- 错误处理逻辑相对基础
### 推荐实践
1. **简化Pipeline结构**
- 避免过于复杂的依赖关系
- 使用清晰的stage分层
2. **充分测试配置**
- 在推送前验证YAML语法
- 使用GitLab CI Lint工具
3. **监控Pipeline执行**
- 定期检查pipeline执行状态
- 及时处理失败的任务
## 升级路径
如果后续升级GitLab版本,可以考虑以下优化:
### GitLab 12.3+
- 使用 `needs` 替代 `dependencies`
- 支持更精细的DAG (有向无环图) 控制
### GitLab 12.8+
- 使用 `rules` 替代 `only/except`
- 支持更复杂的条件逻辑
### GitLab 13.0+
- 支持更多的pipeline特性
- 更好的可视化和监控功能
---
## 总结
当前配置完全兼容GitLab 12.0.3,提供了:
-**稳定的CI/CD流程**
-**完整的测试覆盖**
-**详细的测试报告**
-**灵活的手动触发选项**
-**良好的错误调试支持**
可以安全地提交和使用!
\ No newline at end of file
# GitLab CI 集成 Cypress 测试使用指南
## 概述
本项目已成功集成 GitLab CI/CD 与 Cypress E2E 测试,支持自动化测试、手动触发和详细的测试报告生成。
## 配置文件
- `.gitlab-ci.yml` - GitLab CI 配置文件
- `cypress.config.js` - Cypress 测试配置
- `package.json` - 项目依赖和脚本配置
## 测试策略
### 1. 基础测试 (自动触发)
- **触发条件**: 推送到 `projects` 分支、MR、手动触发
- **测试范围**: 登录、仪表盘、导航等核心功能
- **测试文件**: `{login,dashboard,navigation}.cy.js`
### 2. 完整测试套件 (手动触发)
- **触发条件**: 设置环境变量 `FULL_TEST_SUITE=true`
- **测试范围**: 所有测试用例
- **测试文件**: `cypress/e2e/*.cy.js`
### 3. 并行测试 (手动触发)
- **触发条件**: 设置环境变量 `PARALLEL_TESTS=true`
- **业务功能组**: 除尘器概览、设备管理、监控
- **数据管理组**: 采集器、闭环管理、告警
## 使用方法
### 手动触发测试
1. **进入 GitLab 项目页面**
```
https://app.bmetech.com/liuzhaohui/dctomproject
```
2. **导航到 CI/CD → Pipelines**
3. **点击 "Run Pipeline" 按钮**
4. **设置环境变量**(可选):
- `FULL_TEST_SUITE`: `true` - 运行完整测试套件
- `PARALLEL_TESTS`: `true` - 启用并行测试
- `CYPRESS_baseUrl`: 自定义测试基础URL(默认: http://localhost:3000)
5. **点击 "Run Pipeline" 开始执行**
### 查看测试结果
#### 1. GitLab CI Pipeline 页面
- 查看各个 job 的执行状态
- 下载 Artifacts(截图、视频、报告)
#### 2. GitLab Pages(推荐)
- **访问地址**: `https://liuzhaohui.gitlab.io/dctomproject/`
- **内容包含**:
- 📊 完整HTML测试报告
- 🎥 测试执行视频
- 📸 失败时的截图
#### 3. 本地下载
- Pipeline → Job → Artifacts → Download
- 解压查看详细报告
## 安装依赖
如需在本地运行测试报告生成功能,请安装以下依赖:
```bash
npm install --save-dev mochawesome mochawesome-merge mochawesome-report-generator
```
然后取消注释 `cypress.config.js` 中的 reporter 配置:
```javascript
// 取消注释以下配置
reporter: 'mochawesome',
reporterOptions: {
reportDir: 'cypress/reports',
overwrite: false,
html: true,
json: true,
timestamp: 'mmddyyyy_HHMMss',
reportTitle: 'DC-TOM Cypress Tests',
reportPageTitle: 'DC-TOM 测试报告'
}
```
## 本地测试脚本
```bash
# 基础测试
npm run cy:run:basic
# 业务功能测试
npm run cy:run:business
# 数据管理测试
npm run cy:run:data
# 完整测试
npm run cy:run:full
# 生成测试报告
npm run test:reports
```
## CI Pipeline 结构
```
stages:
├── build # 构建阶段
├── test # 测试阶段
│ ├── cypress-basic-tests # 基础测试
│ ├── cypress-full-tests # 完整测试(手动)
│ ├── cypress-business-tests # 业务测试(并行)
│ └── cypress-data-tests # 数据测试(并行)
└── reports # 报告生成阶段
├── generate-test-reports # 合并报告
└── pages # 部署到GitLab Pages
```
## 故障排除
### 常见问题
1. **构建失败**
- 检查 Node.js 版本(需要 18+)
- 验证 package.json 语法
2. **测试超时**
- 检查应用启动状态
- 增加 `sleep` 等待时间
3. **报告生成失败**
- 确保测试完成并生成了 JSON 报告
- 检查 mochawesome 依赖安装
### 调试方法
1. **查看 Pipeline 日志**
- 每个 job 都有详细的执行日志
2. **下载失败截图**
- 失败时自动截图保存在 artifacts 中
3. **观看执行视频**
- 完整的测试执行过程录像
## 配置自定义
### 修改测试分组
编辑 `.gitlab-ci.yml` 中的 `--spec` 参数:
```yaml
# 自定义测试文件分组
--spec "cypress/e2e/{your-test-files}.cy.js"
```
### 调整超时时间
修改 `cypress.config.js` 中的超时配置:
```javascript
defaultCommandTimeout: 15000, // 命令超时
pageLoadTimeout: 30000, // 页面加载超时
```
### 自定义报告标题
修改 `reporterOptions` 中的标题配置:
```javascript
reportTitle: '您的测试报告标题',
reportPageTitle: '您的页面标题'
```
## 支持与维护
- **项目地址**: https://app.bmetech.com/liuzhaohui/dctomproject
- **测试报告**: https://liuzhaohui.gitlab.io/dctomproject/
- **技术栈**: Vue 3 + Vite + Cypress + GitLab CI
---
**注意**: 首次运行时可能需要较长时间下载 Docker 镜像和依赖包,后续运行会因为缓存而加快速度。
\ No newline at end of file
# GitLab CI 集成 Cypress 测试 - 实施完成总结
## ✅ 已完成的工作
### 1. GitLab CI 配置文件 (`.gitlab-ci.yml`)
- ✅ 创建完整的 CI/CD 流水线配置
- ✅ 配置构建、测试、报告三个阶段
- ✅ 支持基础测试、完整测试、并行测试三种策略
- ✅ 集成 Docker 容器环境
- ✅ 配置缓存优化策略
- ✅ 设置 GitLab Pages 自动部署
### 2. Package.json 更新
- ✅ 添加测试报告生成相关依赖
- mochawesome
- mochawesome-merge
- mochawesome-report-generator
- ✅ 新增测试脚本命令
- cy:run:basic (基础测试)
- cy:run:business (业务功能测试)
- cy:run:data (数据管理测试)
- cy:run:full (完整测试)
- test:reports (生成报告)
### 3. Cypress 配置优化 (`cypress.config.js`)
- ✅ 添加 CI 环境适配
- ✅ 增加超时时间配置
- ✅ 配置重试机制
- ✅ 设置视频和截图保存路径
- ✅ 预配置测试报告格式(待依赖安装后启用)
### 4. 文档创建
-`GITLAB_CI_GUIDE.md` - 详细使用指南
-`IMPLEMENTATION_SUMMARY.md` - 实施总结(本文档)
## 🔄 触发方式配置
### 自动触发
- ✅ 推送到 `projects` 分支
- ✅ 创建 Merge Request
- ✅ 手动触发 Pipeline
### 手动触发选项
-`FULL_TEST_SUITE=true` - 运行完整测试套件
-`PARALLEL_TESTS=true` - 启用并行测试
-`CYPRESS_baseUrl` - 自定义测试URL
## 📊 测试报告存储
### 短期存储 (GitLab Artifacts)
- ✅ 测试截图: `cypress/screenshots/`
- ✅ 测试视频: `cypress/videos/`
- ✅ 测试报告: `cypress/reports/`
- ✅ 保存时间: 1周-1个月
### 长期存储 (GitLab Pages)
- ✅ 在线访问地址: `https://liuzhaohui.gitlab.io/dctomproject/`
- ✅ 美观的HTML报告界面
- ✅ 媒体文件在线查看
- ✅ 持久化存储
## 🧪 测试分层策略
### 基础测试 (自动执行)
- ✅ 覆盖核心功能: 登录、仪表盘、导航
- ✅ 快速反馈机制
- ✅ 每次推送/MR都执行
### 完整测试 (手动触发)
- ✅ 覆盖所有测试用例
- ✅ 全面功能验证
- ✅ 发布前质量保证
### 并行测试 (手动触发)
- ✅ 业务功能组: 除尘器、设备管理、监控
- ✅ 数据管理组: 采集器、闭环管理、告警
- ✅ 提高测试执行效率
## 🏗️ CI Pipeline 架构
```
GitLab CI Pipeline
├── 📦 Build Stage
│ └── build (构建应用)
├── 🧪 Test Stage
│ ├── cypress-basic-tests (基础测试)
│ ├── cypress-full-tests (完整测试-手动)
│ ├── cypress-business-tests (业务测试-并行)
│ └── cypress-data-tests (数据测试-并行)
└── 📋 Reports Stage
├── generate-test-reports (生成报告)
└── pages (部署到GitLab Pages)
```
## 🚀 下一步操作
### 立即可用
1. **提交代码到 GitLab**
```bash
git add .gitlab-ci.yml package.json cypress.config.js *.md
git commit -m "feat: 集成Cypress测试到GitLab CI"
git push origin projects
```
2. **首次运行测试**
- 访问 GitLab 项目页面
- 进入 CI/CD → Pipelines
- 点击 "Run Pipeline" 开始测试
### 可选优化
1. **安装测试报告依赖**(如果本地需要)
```bash
npm install --save-dev mochawesome mochawesome-merge mochawesome-report-generator
```
2. **启用详细报告**
- 取消注释 `cypress.config.js` 中的 reporter 配置
3. **自定义测试分组**
- 根据项目需要调整 `.gitlab-ci.yml` 中的测试文件分组
## 🔧 技术特性
### 性能优化
- ✅ 智能缓存策略 (node_modules, Cypress binary)
- ✅ 并行测试执行
- ✅ 增量构建支持
### 错误处理
- ✅ 失败时自动截图
- ✅ 完整测试过程录像
- ✅ 重试机制 (CI环境2次重试)
- ✅ 详细错误日志
### 报告系统
- ✅ HTML格式美观报告
- ✅ 测试统计数据
- ✅ 失败原因分析
- ✅ 媒体文件集成
## 🎯 项目兼容性
### 环境要求
- ✅ Node.js 18+
- ✅ Docker 容器支持
- ✅ Chrome 浏览器 (CI环境)
- ✅ GitLab CI/CD
### 技术栈兼容
- ✅ Vue 3.5.13
- ✅ Vite 6.3.5
- ✅ Cypress 13.6.0
- ✅ Element Plus 2.9.10
## 📈 预期收益
### 开发效率
- 🚀 自动化测试减少手动测试时间 70%
- 🚀 快速问题定位和修复
- 🚀 持续集成保证代码质量
### 质量保证
- 🛡️ 发布前全面功能验证
- 🛡️ 回归测试自动化
- 🛡️ 用户体验质量监控
### 团队协作
- 👥 统一的测试标准和流程
- 👥 可视化测试报告便于沟通
- 👥 自动化减少人工错误
---
## 🎉 集成完成
GitLab CI 与 Cypress 测试的集成已经完成,现在您可以:
1.**立即提交代码并运行首次测试**
2.**通过 GitLab Pages 查看美观的测试报告**
3.**享受自动化测试带来的效率提升**
测试报告访问地址: `https://liuzhaohui.gitlab.io/dctomproject/`
如有任何问题,请参考 `GITLAB_CI_GUIDE.md` 详细使用指南。
\ No newline at end of file
# 本地 CI 测试使用指南
## 概述
由于 GitLab 环境缺乏 Runner,我们提供了一个本地 CI 脚本来模拟 GitLab CI 流程,可以在本地环境运行完整的测试并生成测试报告。
## 快速开始
### 1. 环境要求
- **Node.js**: 18+
- **npm**: 最新版本
- **Chrome浏览器**: 用于 Cypress 测试
- **操作系统**: macOS, Linux 或 Windows (WSL)
### 2. 使用方法
#### 方法一: 使用 Bash 脚本 (推荐)
```bash
# 运行所有测试类型
./local-ci.sh
# 只运行基础测试 (登录、仪表盘、导航)
./local-ci.sh basic
# 只运行完整测试套件
./local-ci.sh full
# 只运行业务功能测试 (除尘器、设备管理、监控)
./local-ci.sh business
# 只运行数据管理测试 (采集器、闭环管理、告警)
./local-ci.sh data
# 查看帮助
./local-ci.sh help
```
#### 方法二: 使用 npm 脚本
```bash
# 运行所有测试
npm run ci:local
# 运行基础测试
npm run ci:basic
# 运行完整测试
npm run ci:full
# 运行业务功能测试
npm run ci:business
# 运行数据管理测试
npm run ci:data
```
## 执行流程
本地 CI 脚本会按以下顺序执行:
```
1. 🔍 环境依赖检查
├── Node.js 版本验证
├── npm 可用性检查
└── 项目文件存在性验证
2. 📦 构建阶段
├── 安装项目依赖 (npm ci)
├── 构建生产版本 (npm run build)
└── 验证构建结果
3. 🚀 启动应用服务器
├── 启动预览服务器 (npm run preview)
├── 健康检查 (curl localhost:3000)
└── 确认服务器可用
4. 🧪 执行 Cypress 测试
├── 运行指定的测试套件
├── 生成测试视频和截图
└── 生成 mochawesome 报告
5. 📊 生成测试报告
├── 安装报告生成工具
├── 合并所有测试报告
├── 生成 HTML 格式报告
├── 复制媒体文件 (视频/截图)
└── 生成索引页面
6. 🌐 打开测试报告
└── 自动在浏览器中打开报告
```
## 测试分类
### 基础测试 (basic)
- **范围**: 核心功能验证
- **包含**: 登录、仪表盘、导航
- **执行时间**: ~5-10分钟
- **用途**: 快速验证核心功能
### 完整测试 (full)
- **范围**: 所有测试用例
- **包含**: cypress/e2e/*.cy.js
- **执行时间**: ~20-30分钟
- **用途**: 全面功能验证
### 业务功能测试 (business)
- **范围**: 业务核心功能
- **包含**: 除尘器概览、设备管理、监控
- **执行时间**: ~10-15分钟
- **用途**: 业务逻辑验证
### 数据管理测试 (data)
- **范围**: 数据相关功能
- **包含**: 采集器、闭环管理、告警
- **执行时间**: ~10-15分钟
- **用途**: 数据流程验证
## 生成的报告
### 报告结构
```
public/
├── index.html # 主报告页面
├── reports/
│ ├── merged-report.html # 详细测试报告
│ └── merged-report.json # 报告数据
└── assets/
├── videos/ # 测试执行视频
└── screenshots/ # 失败时截图
```
### 报告内容
- **测试概览**: 通过率、失败率、执行时间
- **详细结果**: 每个测试用例的执行状态
- **失败分析**: 失败用例的错误信息和截图
- **执行视频**: 完整的测试执行过程录像
- **环境信息**: 浏览器版本、测试配置等
## 故障排除
### 常见问题
#### 1. 权限错误
```bash
# 解决方案: 给脚本添加执行权限
chmod +x local-ci.sh
```
#### 2. 端口被占用
```bash
# 解决方案: 关闭占用3000端口的进程
lsof -ti:3000 | xargs kill
```
#### 3. Chrome浏览器未找到
```bash
# macOS 解决方案
brew install --cask google-chrome
# Ubuntu 解决方案
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo apt-get update && sudo apt-get install google-chrome-stable
```
#### 4. 依赖安装失败
```bash
# 清除缓存重新安装
rm -rf node_modules package-lock.json
npm install
```
#### 5. 测试超时
```bash
# 增加 Cypress 超时时间 (在 cypress.config.js 中)
defaultCommandTimeout: 20000
pageLoadTimeout: 60000
```
### 调试模式
如果需要调试特定测试,可以:
```bash
# 1. 手动启动服务器
npm run preview
# 2. 在另一个终端打开 Cypress GUI
npm run cy:open
# 3. 在 GUI 中选择要调试的测试
```
## 与 GitLab CI 的对比
| 功能 | GitLab CI | 本地 CI | 说明 |
|------|-----------|---------|------|
| 环境隔离 | ✅ Docker容器 | ❌ 本地环境 | 本地可能受环境影响 |
| 并行执行 | ✅ 多Runner并行 | ❌ 单线程执行 | 本地执行时间较长 |
| 缓存机制 | ✅ 分布式缓存 | ✅ 本地缓存 | 两者都支持依赖缓存 |
| 报告存储 | ✅ Artifacts+Pages | ✅ 本地文件 | 功能基本相同 |
| 自动触发 | ✅ Git事件触发 | ❌ 手动执行 | 本地需要手动运行 |
| 调试便利性 | ❌ 远程调试困难 | ✅ 本地调试方便 | 本地更便于调试 |
## 最佳实践
### 1. 开发阶段
- 使用 `npm run ci:basic` 进行快速验证
- 修复问题后使用 `npm run ci:full` 进行全面测试
### 2. 发布前
- 运行 `./local-ci.sh` 执行完整的测试流程
- 检查生成的报告确认所有测试通过
### 3. 持续改进
- 定期检查测试执行时间,优化慢速测试
- 分析失败模式,改进测试用例
- 根据项目发展调整测试分组策略
## 自动化增强
可以考虑添加以下自动化功能:
### 1. 定时执行
```bash
# 使用 cron 定时执行
0 2 * * * cd /path/to/project && ./local-ci.sh full
```
### 2. Git Hook 集成
```bash
# 在 .git/hooks/pre-push 中添加
#!/bin/sh
./local-ci.sh basic
```
### 3. Slack 通知
```bash
# 在脚本末尾添加通知
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"测试完成: '"$test_result"'"}' \
YOUR_SLACK_WEBHOOK_URL
```
---
## 总结
本地 CI 脚本提供了一个完整的解决方案,可以在 GitLab Runner 不可用的情况下:
**完整模拟 GitLab CI 流程**
**生成专业的测试报告**
**支持多种测试策略**
**提供详细的调试信息**
**易于使用和扩展**
现在您可以在本地环境获得与 GitLab CI 相同的测试体验!
\ No newline at end of file
# 本地 CI 解决方案总结
## 问题背景
由于 GitLab 环境缺乏 Runner,无法正常执行 CI/CD 流水线,需要一个本地解决方案来:
- 运行 Cypress E2E 测试
- 生成测试报告
- 模拟 GitLab CI 流程
## 解决方案
### 🎯 创建的文件
1. **`local-ci.sh`** - 主要的本地 CI 脚本
- 完整模拟 GitLab CI 流程
- 支持多种测试策略
- 自动生成测试报告
2. **`quick-test.sh`** - 快速测试脚本
- 用于单个测试文件验证
- 快速调试和开发
3. **`LOCAL_CI_GUIDE.md`** - 详细使用指南
- 完整的使用说明
- 故障排除指南
- 最佳实践建议
4. **更新的 `package.json`** - 新增 npm 脚本
- 便于使用的 npm 命令
- 统一的脚本管理
### 🚀 使用方法
#### 完整测试流程
```bash
# 运行所有测试 (推荐用于发布前验证)
./local-ci.sh
# 只运行基础测试 (快速验证)
./local-ci.sh basic
npm run ci:basic
# 运行特定测试类型
./local-ci.sh business # 业务功能测试
./local-ci.sh data # 数据管理测试
./local-ci.sh full # 完整测试套件
```
#### 快速测试 (开发调试)
```bash
# 快速测试单个文件
./quick-test.sh cypress/e2e/login.cy.js
npm run test:login
# 快速测试仪表盘
npm run test:dashboard
# 默认快速测试
npm run test:quick
```
### 📊 生成的报告
#### 完整报告 (local-ci.sh)
```
public/
├── index.html # 主报告页面
├── reports/
│ ├── merged-report.html # 详细测试报告
│ └── merged-report.json # 报告数据
└── assets/
├── videos/ # 测试执行视频
└── screenshots/ # 失败时截图
```
#### 快速报告 (quick-test.sh)
```
public/quick-reports/
├── index.html # 简单报告索引
└── *.html # 单个测试报告
```
### 🔧 技术特性
#### 自动化流程
1. **环境检查** - Node.js、npm、项目文件验证
2. **依赖管理** - 自动安装和缓存依赖
3. **应用构建** - 自动构建和验证
4. **服务启动** - 自动启动预览服务器并健康检查
5. **测试执行** - 运行 Cypress 测试并生成报告
6. **报告生成** - 合并报告并生成 HTML 格式
7. **结果展示** - 自动打开浏览器查看报告
#### 错误处理
- **服务器启动失败**: 自动重试和错误提示
- **测试失败**: 继续执行并记录失败信息
- **依赖问题**: 自动安装缺失的全局工具
- **端口占用**: 清晰的错误提示和解决建议
#### 兼容性
- **操作系统**: macOS, Linux, Windows (WSL)
- **浏览器**: Chrome (主要), Firefox (备选)
- **Node.js**: 18+ (与项目要求一致)
### 📈 优势对比
| 功能 | GitLab CI | 本地 CI 脚本 | 快速测试脚本 |
|------|-----------|-------------|-------------|
| **环境隔离** | ✅ Docker | ❌ 本地环境 | ❌ 本地环境 |
| **执行速度** | 中等 | 快 (本地) | 最快 |
| **调试便利** | 困难 | ✅ 容易 | ✅ 最容易 |
| **报告质量** | ✅ 完整 | ✅ 完整 | 简单 |
| **并行执行** | ✅ 支持 | ❌ 单线程 | ❌ 单线程 |
| **自动触发** | ✅ Git事件 | ❌ 手动 | ❌ 手动 |
| **依赖管理** | ✅ 自动 | ✅ 自动 | ✅ 智能缓存 |
| **适用场景** | 生产环境 | 开发/测试 | 开发调试 |
### 🎨 使用场景建议
#### 开发阶段
```bash
# 修改代码后快速验证
npm run test:quick
# 验证特定功能
npm run test:login
npm run test:dashboard
```
#### 测试阶段
```bash
# 运行基础测试验证主要功能
npm run ci:basic
# 运行特定模块测试
npm run ci:business
npm run ci:data
```
#### 发布前
```bash
# 运行完整测试流程
npm run ci:local
# 或
./local-ci.sh
```
### 🔍 监控和改进
#### 性能监控
- 监控测试执行时间
- 识别慢速测试用例
- 优化测试顺序和分组
#### 质量提升
- 分析失败模式
- 改进测试用例覆盖率
- 优化测试数据和环境
#### 自动化增强
- 考虑添加 Git hooks 集成
- 实现定时测试执行
- 添加测试结果通知
### 📋 维护清单
#### 定期检查
- [ ] 更新 Cypress 和相关依赖
- [ ] 验证测试用例的有效性
- [ ] 检查报告生成工具版本
- [ ] 优化脚本性能和错误处理
#### 扩展功能
- [ ] 添加性能测试集成
- [ ] 实现多浏览器测试支持
- [ ] 添加测试覆盖率报告
- [ ] 集成代码质量检查
---
## 🎉 总结
本地 CI 解决方案提供了一个完整的替代方案,在 GitLab Runner 不可用的情况下:
**完全模拟 GitLab CI 流程**
**生成专业级测试报告**
**支持灵活的测试策略**
**提供便捷的调试能力**
**易于使用和维护**
现在您可以在本地环境获得与 GitLab CI 相同的测试体验,确保代码质量和功能稳定性!
**下一步建议**:
1. 先运行 `npm run ci:basic` 验证基本功能
2. 成功后运行 `npm run ci:local` 执行完整测试
3. 查看生成的报告了解项目测试状况
4. 根据需要调整测试策略和配置
\ No newline at end of file
# 自然语言测试描述指南
## 🎯 概述
DC-TOM项目现在支持使用中文自然语言描述来生成Cypress测试代码,让测试编写变得更加直观和易懂。
## 📝 语法格式
### 基本结构
```
目标页面:[页面名称]
测试内容:
1,[测试步骤描述]
2,[测试步骤描述]
3,[测试步骤描述]
...
```
### 支持的页面模块
| 页面名称 | 对应路由 | 说明 |
|---------|---------|------|
| 首页 | dashboard | 仪表盘主页 |
| 仪表盘 | dashboard | 仪表盘页面 |
| 告警总览 | alerts | 告警管理页面 |
| 设备管理 | management/device-management | 设备管理页面 |
| 布袋周期 | collectorList | 布袋周期管理页面 |
| 除尘器总览 | dust-overview | 除尘器管理页面 |
| 除尘监测 | monitor | 实时监测页面 |
| 闭环管理 | my-loop | 工作流管理页面 |
### 支持的操作动词
| 动词 | 说明 | 生成的Cypress操作 |
|------|------|------------------|
| 查询/搜索/检索 | 搜索操作 | `cy.get().click()` (搜索按钮) |
| 查看/检查/验证 | 验证操作 | `cy.get().should('be.visible')` |
| 点击/单击 | 点击操作 | `cy.get().click()` |
| 输入/填写 | 输入操作 | `cy.get().type()` |
| 选择 | 选择操作 | `cy.get().select()` |
### 支持的数据对象
| 对象名称 | 生成的选择器 | 说明 |
|---------|-------------|------|
| 布袋健康度 | `[data-testid="dashboard-bag-progress"]` | 布袋健康度指标 |
| 综合健康度 | `[data-testid="dashboard-health-score"]` | 综合健康度评分 |
| 布袋总数 | `[data-testid*="bag-total"]` | 布袋总数统计 |
| 告警数据 | `[data-testid*="alert-table"]` | 告警数据表格 |
| 顶部区域 | `.top-area, .header-area` | 页面顶部区域 |
### 支持的时间范围
| 时间描述 | 处理方式 | 说明 |
|---------|----------|------|
| 今日/今天 | 选择今天日期 | 自动点击日期选择器选择今日 |
| 昨日/昨天 | 选择昨天日期 | 选择昨天的日期 |
| 本周 | 选择本周范围 | 选择本周时间范围 |
| 本月 | 选择本月范围 | 选择本月时间范围 |
## 📖 使用示例
### 示例1:仪表盘综合测试
```
目标页面:首页
测试内容:
1,在告警总览内查询今日布袋告警数据。
2,查询布袋总数
3,在首页查看顶部区域综合健康度的布袋健康度
```
**生成的测试代码:**
```javascript
/// <reference types="cypress" />
describe('首页功能测试', () => {
beforeEach(() => {
cy.mockLogin()
cy.visit('/#/dashboard')
cy.get('[data-testid="dashboard-container"]').should('be.visible')
})
it('应该能够在告警总览内查询今日布袋告警数据', () => {
// 切换到alerts模块
cy.visit('/#/alerts')
cy.wait(1000)
// 在告警总览内查询今日布袋告警数据
cy.get('[data-testid*="date-picker"]').click()
cy.get('.el-picker-panel').should('be.visible')
// 选择今日日期
cy.get('.el-date-table td.today').click()
cy.get('[data-testid*="search-button"]').click()
cy.wait(1000)
// 验证结果
cy.get('[data-testid*="alert-table"]').should('be.visible')
})
it('应该能够查询布袋总数', () => {
// 查询布袋总数
// 验证结果
cy.get('[data-testid*="bag-total"]').should('be.visible')
})
it('应该能够在首页查看顶部区域综合健康度的布袋健康度', () => {
// 在首页查看顶部区域综合健康度的布袋健康度
cy.get('[data-testid="dashboard-bag-progress"]').should('be.visible')
// 验证结果
cy.get('[data-testid="dashboard-bag-progress"]').should('be.visible')
})
})
```
### 示例2:设备管理测试
```
目标页面:设备管理
测试内容:
1,搜索设备名称为"除尘器001"的设备
2,验证搜索结果表格显示正常
3,点击重置按钮清空搜索条件
```
### 示例3:布袋周期测试
```
目标页面:布袋周期
测试内容:
1,在仓室输入框中输入"A仓室"
2,在除尘器名称输入框中输入"1#除尘器"
3,点击查询按钮执行搜索
4,验证表格显示搜索结果
5,点击更换周期分析按钮打开对话框
```
## 🚀 使用方法
### 方法1:Web界面(推荐)
1. 启动Web界面:`npm run test-gen:web`
2. 点击"自然语言"标签页
3. 输入中文描述
4. 点击"解析生成测试代码"
5. 复制或下载生成的代码
### 方法2:命令行工具
```bash
# 1. 创建自然语言描述文件(.txt格式)
echo "目标页面:首页
测试内容:
1,查看布袋健康度指标
2,验证数据加载正常" > my-test.txt
# 2. 使用CLI工具解析
cd cypress/test-generator
node cli.js parse my-test.txt
# 3. 或使用集成脚本
./test-generator.sh parse test-descriptions/my-test.txt
```
### 方法3:npm脚本
```bash
# 生成自然语言示例
npm run test-gen:nl-example
# 批量解析所有.txt文件
npm run test-gen:parse
```
## 🎨 高级用法
### 1. 跨模块测试
```
目标页面:首页
测试内容:
1,在首页查看健康度指标
2,切换到告警总览查询告警数据
3,返回首页验证数据一致性
```
### 2. 条件测试
```
目标页面:设备管理
测试内容:
1,如果设备列表为空,显示空状态提示
2,搜索不存在的设备名称
3,验证显示"无数据"提示
```
### 3. 工作流测试
```
目标页面:布袋周期
测试内容:
1,查询即将到期的布袋
2,选择需要更换的布袋
3,提交更换申请
4,验证状态更新
```
## 🔧 自定义扩展
### 添加新的操作动词
`natural-language-parser.js` 中的 `actionPatterns` 数组添加:
```javascript
{
pattern: /拖拽|拖动/,
action: 'drag',
description: '拖拽操作'
}
```
### 添加新的数据对象
`dataPatterns` 数组添加:
```javascript
{
pattern: /新的数据类型/,
type: 'new_data_type',
selector: '[data-testid="new-data-selector"]'
}
```
### 添加新的页面模块
`moduleMapping` 对象添加:
```javascript
'新页面': {
route: 'new-page',
testId: 'new-page',
moduleName: '新页面'
}
```
## 📋 最佳实践
### 1. 描述要清晰具体
✅ 好的描述:
```
在布袋健康度指标中查看当前状态
```
❌ 模糊的描述:
```
看看页面
```
### 2. 使用标准的操作动词
✅ 推荐:
```
1,查询今日告警数据
2,验证表格显示正常
3,点击重置按钮
```
❌ 不推荐:
```
1,弄一下告警数据
2,看看表格对不对
3,搞一下重置
```
### 3. 保持逻辑顺序
✅ 有逻辑的顺序:
```
1,输入搜索条件
2,点击搜索按钮
3,验证搜索结果
4,重置搜索条件
```
### 4. 指定具体的目标
✅ 具体的目标:
```
在仓室输入框中输入"A仓室"
```
❌ 模糊的目标:
```
输入一些内容
```
## 🐛 常见问题
### Q: 解析失败怎么办?
A: 检查以下几点:
- 是否使用了支持的页面名称
- 是否使用了支持的操作动词
- 描述格式是否正确
- 是否包含"目标页面"和"测试内容"
### Q: 生成的选择器不准确?
A: 可以在自然语言解析器中添加更具体的选择器映射,或者在生成后手动调整选择器。
### Q: 如何测试复杂的交互流程?
A: 将复杂流程分解为多个简单步骤,每个步骤使用一个具体的操作动词。
### Q: 支持英文描述吗?
A: 目前主要支持中文描述,英文支持需要扩展解析器的模式匹配。
## 🔮 未来计划
- [ ] 支持更多复杂的条件逻辑
- [ ] 添加更多Element Plus组件的智能识别
- [ ] 支持测试数据的动态生成
- [ ] 集成AI模型提升解析准确度
- [ ] 支持多语言描述(英文、日文等)
---
通过这种自然语言描述方式,您可以用最直观的中文来描述测试需求,系统会自动生成符合项目规范的Cypress测试代码,大大降低了测试编写的技术门槛!
\ No newline at end of file
# DC-TOM 测试描述转测试脚本流程架构实施总结
## 🎯 项目目标
在 DC-TOM 项目基础上构建一个完整的测试描述转测试脚本的流程架构,使用JSON输入格式,实现自动化测试代码生成。
## 🏗️ 系统架构
### 整体架构图
```mermaid
graph TB
A[JSON测试描述] --> B[Schema验证]
B --> C[测试代码生成器]
C --> D[模板引擎]
D --> E[Cypress测试代码]
E --> F[测试执行]
F --> G[测试报告]
H[Web界面] --> A
I[CLI工具] --> A
J[示例文件] --> A
subgraph "核心组件"
B
C
D
end
subgraph "用户界面"
H
I
J
end
```
### 目录结构
```
cypress/test-generator/
├── config.js # 核心配置文件
├── schema.json # JSON Schema验证规范
├── template-engine.js # 模板引擎
├── test-code-generator.js # 代码生成器
├── cli.js # 命令行工具
├── web-interface.html # Web可视化界面
└── README.md # 详细文档
test-descriptions/ # JSON测试描述目录
├── collector-test-complete.json
├── dashboard-test.json
└── device-management-test.json
cypress/e2e/generated/ # 生成的测试文件目录
test-generator.sh # 集成脚本
```
## 🔧 核心组件
### 1. 配置管理 (config.js)
- **功能**: 集中管理项目配置、模块映射、选择器模板
- **特性**:
- 模块路由映射 (登录→login, 仪表盘→dashboard等)
- 测试ID前缀映射
- 通用选择器模板
- Element Plus组件选择器
### 2. JSON Schema验证 (schema.json)
- **功能**: 定义JSON测试描述的标准格式
- **支持的测试类型**: ui, interaction, data, error, performance, responsive
- **操作类型**: click, type, select, verify, wait, intercept, custom
- **断言类型**: be.visible, exist, contain, have.attr等
### 3. 模板引擎 (template-engine.js)
- **功能**: 管理和渲染测试代码模板
- **模板类型**:
- 基础模板 (base)
- beforeEach模板
- UI验证模板
- 交互测试模板
- 数据验证模板
- 错误处理模板
- 响应式设计模板
- 性能测试模板
### 4. 代码生成器 (test-code-generator.js)
- **功能**: 将JSON描述转换为Cypress测试代码
- **核心方法**:
- `generateFromJSON()`: 主生成方法
- `validateTestDescription()`: JSON验证
- `buildTemplateContext()`: 构建模板上下文
- `processSteps()`: 处理测试步骤
- `generateTestFile()`: 生成文件
### 5. CLI工具 (cli.js)
- **功能**: 命令行操作界面
- **支持命令**:
- `generate`: 生成测试代码
- `validate`: 验证JSON格式
- `example`: 生成示例文件
- `help`: 显示帮助
### 6. Web界面 (web-interface.html)
- **功能**: 可视化测试生成界面
- **特性**:
- 实时JSON验证
- 代码预览
- 示例加载
- 代码复制和下载
## 📋 JSON测试描述格式
### 基本结构
```json
{
"testSuite": {
"name": "测试套件名称",
"module": "模块名称",
"description": "测试描述",
"beforeEach": {
"login": true,
"visit": "页面路径",
"waitFor": ["选择器数组"]
},
"scenarios": [
{
"name": "测试场景名称",
"type": "测试类型",
"steps": [],
"expectedResults": []
}
]
}
}
```
### 支持的模块
- 登录、仪表盘、除尘器总览、布袋周期
- 除尘监测、设备管理、闭环管理、告警总览
### 测试类型
- **ui**: UI组件验证
- **interaction**: 用户交互测试
- **data**: 数据验证测试
- **error**: 错误处理测试
- **performance**: 性能测试
- **responsive**: 响应式设计测试
## 🚀 使用流程
### 1. 环境初始化
```bash
npm run test-gen:setup
```
### 2. 生成示例文件
```bash
npm run test-gen:example
```
### 3. 创建JSON测试描述
- 使用Web界面: `npm run test-gen:web`
- 手动编写JSON文件放入 `test-descriptions/` 目录
### 4. 验证JSON格式
```bash
npm run test-gen:validate
```
### 5. 生成测试代码
```bash
npm run test-gen:generate
```
### 6. 运行生成的测试
```bash
npm run test-gen:run
```
## 🎨 选择器占位符系统
为简化选择器编写,系统提供占位符:
| 占位符 | 替换结果 | 说明 |
|--------|----------|------|
| `{module}` | 模块ID | 当前模块的testid前缀 |
| `{container}` | `[data-testid="{module}-container"]` | 主容器 |
| `{searchForm}` | `[data-testid="{module}-search-form"]` | 搜索表单 |
| `{table}` | `[data-testid="{module}-common-table"]` | 数据表格 |
| `{searchButton}` | `[data-testid="{module}-search-button"]` | 搜索按钮 |
| `{resetButton}` | `[data-testid="{module}-reset-button"]` | 重置按钮 |
## 📊 生成的测试代码特点
### 1. 标准化结构
```javascript
/// <reference types="cypress" />
describe('测试套件名称', () => {
beforeEach(() => {
cy.mockLogin()
cy.visit('/#/路径')
cy.get('[data-testid="容器"]').should('be.visible')
})
it('测试场景名称', () => {
// 测试步骤
// 结果验证
})
})
```
### 2. 智能选择器处理
- 自动替换占位符
- 支持Element Plus组件选择器
- 兼容现有项目的data-testid规范
### 3. 错误处理支持
- API拦截和模拟
- 网络错误模拟
- 超时处理
### 4. 性能和响应式测试
- 页面加载时间验证
- 多视口尺寸测试
- 自动化性能监控
## 🔗 集成到现有CI/CD
### 本地CI集成
生成的测试自动集成到现有的本地CI脚本:
```bash
./local-ci.sh generated # 运行生成的测试
```
### package.json脚本
新增的npm脚本:
- `test-gen:setup`: 环境初始化
- `test-gen:example`: 生成示例
- `test-gen:validate`: 验证JSON
- `test-gen:generate`: 生成测试
- `test-gen:run`: 运行测试
- `test-gen:web`: Web界面
- `cy:run:generated`: 运行生成的测试
## 📈 扩展性设计
### 1. 模块扩展
```javascript
// config.js中添加新模块
moduleRoutes: {
'新模块': 'new-module-route'
},
moduleTestIds: {
'新模块': 'new-module'
}
```
### 2. 测试类型扩展
```javascript
// template-engine.js中添加新模板
createNewTestTemplate() {
return `新测试类型的模板`;
}
```
### 3. 操作类型扩展
```javascript
// test-code-generator.js中添加新操作
case 'newAction':
stepCode += `cy.newCustomAction('${step.value}')\n`;
break;
```
## ✅ 已完成功能
### 核心功能
- ✅ JSON Schema定义和验证
- ✅ 多种测试类型支持
- ✅ 模板引擎系统
- ✅ 代码生成器
- ✅ CLI工具
- ✅ Web可视化界面
- ✅ 集成脚本
### 测试支持
- ✅ UI验证测试
- ✅ 交互功能测试
- ✅ 数据验证测试
- ✅ 错误处理测试
- ✅ 性能测试
- ✅ 响应式设计测试
### 项目集成
- ✅ package.json脚本集成
- ✅ 现有CI/CD兼容
- ✅ 目录结构规范
- ✅ 文档完整性
## 🎯 技术亮点
### 1. 架构设计
- **模块化设计**: 各组件职责清晰,便于维护扩展
- **配置驱动**: 通过配置文件管理项目特定设置
- **模板化生成**: 使用模板引擎确保代码一致性
### 2. 用户体验
- **多种使用方式**: CLI、Web界面、脚本集成
- **实时验证**: JSON格式实时检查和错误提示
- **示例驱动**: 丰富的示例帮助快速上手
### 3. 代码质量
- **标准化输出**: 生成的测试代码符合Cypress最佳实践
- **错误处理**: 完善的错误检查和用户友好提示
- **文档完整**: 详细的使用说明和API文档
### 4. 项目兼容性
- **无侵入性**: 不影响现有测试代码
- **向后兼容**: 兼容现有的CI/CD流程
- **渐进增强**: 可以逐步迁移现有测试
## 🔮 未来规划
### 短期优化
- [ ] 增加更多Element Plus组件支持
- [ ] 优化模板引擎性能
- [ ] 添加测试覆盖率统计
### 中期发展
- [ ] 支持Visual Testing
- [ ] API测试自动生成
- [ ] 性能基准测试集成
### 长期愿景
- [ ] AI驱动的测试生成
- [ ] 跨项目模板共享
- [ ] 云端测试生成服务
## 🏆 总结
本次实施成功构建了一个完整的测试描述转测试脚本的流程架构,具备以下核心价值:
1. **提升效率**: 从手动编写测试到JSON驱动自动生成,大幅提升测试开发效率
2. **保证质量**: 标准化的模板确保生成的测试代码质量和一致性
3. **降低门槛**: 通过JSON描述和Web界面,降低测试编写的技术门槛
4. **易于维护**: 模块化设计和配置驱动,便于后续维护和扩展
5. **无缝集成**: 与现有项目CI/CD流程完美集成,不影响现有工作流
这套系统为DC-TOM项目的测试自动化奠定了坚实基础,同时也为其他类似项目提供了可复用的解决方案。
\ No newline at end of file
/// <reference types="cypress" />
// Welcome to Cypress!
//
// This spec file contains a variety of sample tests
// for a todo list app that are designed to demonstrate
// the power of writing tests in Cypress.
//
// To learn more about how Cypress works and
// what makes it such an awesome testing tool,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress
describe('example to-do app', () => {
beforeEach(() => {
// Cypress starts out with a blank slate for each test
// so we must tell it to visit our website with the `cy.visit()` command.
// Since we want to visit the same URL at the start of all our tests,
// we include it in our beforeEach function so that it runs before each test
cy.visit('https://example.cypress.io/todo')
})
it('displays two todo items by default', () => {
// We use the `cy.get()` command to get all elements that match the selector.
// Then, we use `should` to assert that there are two matched items,
// which are the two default items.
cy.get('.todo-list li').should('have.length', 2)
// We can go even further and check that the default todos each contain
// the correct text. We use the `first` and `last` functions
// to get just the first and last matched elements individually,
// and then perform an assertion with `should`.
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
})
it('can add new todo items', () => {
// We'll store our item text in a variable so we can reuse it
const newItem = 'Feed the cat'
// Let's get the input element and use the `type` command to
// input our new list item. After typing the content of our item,
// we need to type the enter key as well in order to submit the input.
// This input has a data-test attribute so we'll use that to select the
// element in accordance with best practices:
// https://on.cypress.io/selecting-elements
cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
// Now that we've typed our new item, let's check that it actually was added to the list.
// Since it's the newest item, it should exist as the last element in the list.
// In addition, with the two default items, we should have a total of 3 elements in the list.
// Since assertions yield the element that was asserted on,
// we can chain both of these assertions together into a single statement.
cy.get('.todo-list li')
.should('have.length', 3)
.last()
.should('have.text', newItem)
})
it('can check off an item as completed', () => {
// In addition to using the `get` command to get an element by selector,
// we can also use the `contains` command to get an element by its contents.
// However, this will yield the <label>, which is lowest-level element that contains the text.
// In order to check the item, we'll find the <input> element for this <label>
// by traversing up the dom to the parent element. From there, we can `find`
// the child checkbox <input> element and use the `check` command to check it.
cy.contains('Pay electric bill')
.parent()
.find('input[type=checkbox]')
.check()
// Now that we've checked the button, we can go ahead and make sure
// that the list element is now marked as completed.
// Again we'll use `contains` to find the <label> element and then use the `parents` command
// to traverse multiple levels up the dom until we find the corresponding <li> element.
// Once we get that element, we can assert that it has the completed class.
cy.contains('Pay electric bill')
.parents('li')
.should('have.class', 'completed')
})
context('with a checked task', () => {
beforeEach(() => {
// We'll take the command we used above to check off an element
// Since we want to perform multiple tests that start with checking
// one element, we put it in the beforeEach hook
// so that it runs at the start of every test.
cy.contains('Pay electric bill')
.parent()
.find('input[type=checkbox]')
.check()
})
it('can filter for uncompleted tasks', () => {
// We'll click on the "active" button in order to
// display only incomplete items
cy.contains('Active').click()
// After filtering, we can assert that there is only the one
// incomplete item in the list.
cy.get('.todo-list li')
.should('have.length', 1)
.first()
.should('have.text', 'Walk the dog')
// For good measure, let's also assert that the task we checked off
// does not exist on the page.
cy.contains('Pay electric bill').should('not.exist')
})
it('can filter for completed tasks', () => {
// We can perform similar steps as the test above to ensure
// that only completed tasks are shown
cy.contains('Completed').click()
cy.get('.todo-list li')
.should('have.length', 1)
.first()
.should('have.text', 'Pay electric bill')
cy.contains('Walk the dog').should('not.exist')
})
it('can delete all completed tasks', () => {
// First, let's click the "Clear completed" button
// `contains` is actually serving two purposes here.
// First, it's ensuring that the button exists within the dom.
// This button only appears when at least one task is checked
// so this command is implicitly verifying that it does exist.
// Second, it selects the button so we can click it.
cy.contains('Clear completed').click()
// Then we can make sure that there is only one element
// in the list and our element does not exist
cy.get('.todo-list li')
.should('have.length', 1)
.should('not.have.text', 'Pay electric bill')
// Finally, make sure that the clear button no longer exists.
cy.contains('Clear completed').should('not.exist')
})
})
})
This diff is collapsed.
/// <reference types="cypress" />
context('Aliasing', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/aliasing')
})
it('.as() - alias a DOM element for later use', () => {
// https://on.cypress.io/as
// Alias a DOM element for use later
// We don't have to traverse to the element
// later in our code, we reference it with @
cy.get('.as-table').find('tbody>tr')
.first().find('td').first()
.find('button').as('firstBtn')
// when we reference the alias, we place an
// @ in front of its name
cy.get('@firstBtn').click()
cy.get('@firstBtn')
.should('have.class', 'btn-success')
.and('contain', 'Changed')
})
it('.as() - alias a route for later use', () => {
// Alias the route to wait for its response
cy.intercept('GET', '**/comments/*').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// https://on.cypress.io/wait
cy.wait('@getComment').its('response.statusCode').should('eq', 200)
})
})
/// <reference types="cypress" />
context('Assertions', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/assertions')
})
describe('Implicit Assertions', () => {
it('.should() - make an assertion about the current subject', () => {
// https://on.cypress.io/should
cy.get('.assertion-table')
.find('tbody tr:last')
.should('have.class', 'success')
.find('td')
.first()
// checking the text of the <td> element in various ways
.should('have.text', 'Column content')
.should('contain', 'Column content')
.should('have.html', 'Column content')
// chai-jquery uses "is()" to check if element matches selector
.should('match', 'td')
// to match text content against a regular expression
// first need to invoke jQuery method text()
// and then match using regular expression
.invoke('text')
.should('match', /column content/i)
// a better way to check element's text content against a regular expression
// is to use "cy.contains"
// https://on.cypress.io/contains
cy.get('.assertion-table')
.find('tbody tr:last')
// finds first <td> element with text content matching regular expression
.contains('td', /column content/i)
.should('be.visible')
// for more information about asserting element's text
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
})
it('.and() - chain multiple assertions together', () => {
// https://on.cypress.io/and
cy.get('.assertions-link')
.should('have.class', 'active')
.and('have.attr', 'href')
.and('include', 'cypress.io')
})
})
describe('Explicit Assertions', () => {
// https://on.cypress.io/assertions
it('expect - make an assertion about a specified subject', () => {
// We can use Chai's BDD style assertions
expect(true).to.be.true
const o = { foo: 'bar' }
expect(o).to.equal(o)
expect(o).to.deep.equal({ foo: 'bar' })
// matching text using regular expression
expect('FooBar').to.match(/bar$/i)
})
it('pass your own callback function to should()', () => {
// Pass a function to should that can have any number
// of explicit assertions within it.
// The ".should(cb)" function will be retried
// automatically until it passes all your explicit assertions or times out.
cy.get('.assertions-p')
.find('p')
.should(($p) => {
// https://on.cypress.io/$
// return an array of texts from all of the p's
const texts = $p.map((i, el) => Cypress.$(el).text())
// jquery map returns jquery object
// and .get() convert this to simple array
const paragraphs = texts.get()
// array should have length of 3
expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
// use second argument to expect(...) to provide clear
// message with each assertion
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
'Some text from first p',
'More text from second p',
'And even more text from third p',
])
})
})
it('finds element by class name regex', () => {
cy.get('.docs-header')
.find('div')
// .should(cb) callback function will be retried
.should(($div) => {
expect($div).to.have.length(1)
const className = $div[0].className
expect(className).to.match(/heading-/)
})
// .then(cb) callback is not retried,
// it either passes or fails
.then(($div) => {
expect($div, 'text content').to.have.text('Introduction')
})
})
it('can throw any error', () => {
cy.get('.docs-header')
.find('div')
.should(($div) => {
if ($div.length !== 1) {
// you can throw your own errors
throw new Error('Did not find 1 element')
}
const className = $div[0].className
if (!className.match(/heading-/)) {
throw new Error(`Could not find class "heading-" in ${className}`)
}
})
})
it('matches unknown text between two elements', () => {
/**
* Text from the first element.
* @type {string}
*/
let text
/**
* Normalizes passed text,
* useful before comparing text with spaces and different capitalization.
* @param {string} s Text to normalize
*/
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
cy.get('.two-elements')
.find('.first')
.then(($first) => {
// save text from the first element
text = normalizeText($first.text())
})
cy.get('.two-elements')
.find('.second')
.should(($div) => {
// we can massage text before comparing
const secondText = normalizeText($div.text())
expect(secondText, 'second text').to.equal(text)
})
})
it('assert - assert shape of an object', () => {
const person = {
name: 'Joe',
age: 20,
}
assert.isObject(person, 'value is object')
})
it('retries the should callback until assertions pass', () => {
cy.get('#random-number')
.should(($div) => {
const n = parseFloat($div.text())
expect(n).to.be.gte(1).and.be.lte(10)
})
})
})
})
/// <reference types="cypress" />
context('Connectors', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/connectors')
})
it('.each() - iterate over an array of elements', () => {
// https://on.cypress.io/each
cy.get('.connectors-each-ul>li')
.each(($el, index, $list) => {
console.log($el, index, $list)
})
})
it('.its() - get properties on the current subject', () => {
// https://on.cypress.io/its
cy.get('.connectors-its-ul>li')
// calls the 'length' property yielding that value
.its('length')
.should('be.gt', 2)
})
it('.invoke() - invoke a function on the current subject', () => {
// our div is hidden in our script.js
// $('.connectors-div').hide()
cy.get('.connectors-div').should('be.hidden')
// https://on.cypress.io/invoke
// call the jquery method 'show' on the 'div.container'
cy.get('.connectors-div').invoke('show')
cy.get('.connectors-div').should('be.visible')
})
it('.spread() - spread an array as individual args to callback function', () => {
// https://on.cypress.io/spread
const arr = ['foo', 'bar', 'baz']
cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.eq('foo')
expect(bar).to.eq('bar')
expect(baz).to.eq('baz')
})
})
describe('.then()', () => {
it('invokes a callback function with the current subject', () => {
// https://on.cypress.io/then
cy.get('.connectors-list > li')
.then(($lis) => {
expect($lis, '3 items').to.have.length(3)
expect($lis.eq(0), 'first item').to.contain('Walk the dog')
expect($lis.eq(1), 'second item').to.contain('Feed the cat')
expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
})
})
it('yields the returned value to the next command', () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
return 2
})
.then((num) => {
expect(num).to.equal(2)
})
})
it('yields the original subject without return', () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
// note that nothing is returned from this callback
})
.then((num) => {
// this callback receives the original unchanged value 1
expect(num).to.equal(1)
})
})
it('yields the value yielded by the last Cypress command inside', () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
// note how we run a Cypress command
// the result yielded by this Cypress command
// will be passed to the second ".then"
cy.wrap(2)
})
.then((num) => {
// this callback receives the value yielded by "cy.wrap(2)"
expect(num).to.equal(2)
})
})
})
})
/// <reference types="cypress" />
context('Cookies', () => {
beforeEach(() => {
Cypress.Cookies.debug(true)
cy.visit('https://example.cypress.io/commands/cookies')
// clear cookies again after visiting to remove
// any 3rd party cookies picked up such as cloudflare
cy.clearCookies()
})
it('cy.getCookie() - get a browser cookie', () => {
// https://on.cypress.io/getcookie
cy.get('#getCookie .set-a-cookie').click()
// cy.getCookie() yields a cookie object
cy.getCookie('token').should('have.property', 'value', '123ABC')
})
it('cy.getCookies() - get browser cookies for the current domain', () => {
// https://on.cypress.io/getcookies
cy.getCookies().should('be.empty')
cy.get('#getCookies .set-a-cookie').click()
// cy.getCookies() yields an array of cookies
cy.getCookies().should('have.length', 1).should((cookies) => {
// each cookie has these properties
expect(cookies[0]).to.have.property('name', 'token')
expect(cookies[0]).to.have.property('value', '123ABC')
expect(cookies[0]).to.have.property('httpOnly', false)
expect(cookies[0]).to.have.property('secure', false)
expect(cookies[0]).to.have.property('domain')
expect(cookies[0]).to.have.property('path')
})
})
it('cy.getAllCookies() - get all browser cookies', () => {
// https://on.cypress.io/getallcookies
cy.getAllCookies().should('be.empty')
cy.setCookie('key', 'value')
cy.setCookie('key', 'value', { domain: '.example.com' })
// cy.getAllCookies() yields an array of cookies
cy.getAllCookies().should('have.length', 2).should((cookies) => {
// each cookie has these properties
expect(cookies[0]).to.have.property('name', 'key')
expect(cookies[0]).to.have.property('value', 'value')
expect(cookies[0]).to.have.property('httpOnly', false)
expect(cookies[0]).to.have.property('secure', false)
expect(cookies[0]).to.have.property('domain')
expect(cookies[0]).to.have.property('path')
expect(cookies[1]).to.have.property('name', 'key')
expect(cookies[1]).to.have.property('value', 'value')
expect(cookies[1]).to.have.property('httpOnly', false)
expect(cookies[1]).to.have.property('secure', false)
expect(cookies[1]).to.have.property('domain', '.example.com')
expect(cookies[1]).to.have.property('path')
})
})
it('cy.setCookie() - set a browser cookie', () => {
// https://on.cypress.io/setcookie
cy.getCookies().should('be.empty')
cy.setCookie('foo', 'bar')
// cy.getCookie() yields a cookie object
cy.getCookie('foo').should('have.property', 'value', 'bar')
})
it('cy.clearCookie() - clear a browser cookie', () => {
// https://on.cypress.io/clearcookie
cy.getCookie('token').should('be.null')
cy.get('#clearCookie .set-a-cookie').click()
cy.getCookie('token').should('have.property', 'value', '123ABC')
// cy.clearCookies() yields null
cy.clearCookie('token')
cy.getCookie('token').should('be.null')
})
it('cy.clearCookies() - clear browser cookies for the current domain', () => {
// https://on.cypress.io/clearcookies
cy.getCookies().should('be.empty')
cy.get('#clearCookies .set-a-cookie').click()
cy.getCookies().should('have.length', 1)
// cy.clearCookies() yields null
cy.clearCookies()
cy.getCookies().should('be.empty')
})
it('cy.clearAllCookies() - clear all browser cookies', () => {
// https://on.cypress.io/clearallcookies
cy.getAllCookies().should('be.empty')
cy.setCookie('key', 'value')
cy.setCookie('key', 'value', { domain: '.example.com' })
cy.getAllCookies().should('have.length', 2)
// cy.clearAllCookies() yields null
cy.clearAllCookies()
cy.getAllCookies().should('be.empty')
})
})
/// <reference types="cypress" />
context('Cypress APIs', () => {
context('Cypress.Commands', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/custom-commands
it('.add() - create a custom command', () => {
Cypress.Commands.add('console', {
prevSubject: true,
}, (subject, method) => {
// the previous subject is automatically received
// and the commands arguments are shifted
// allow us to change the console method used
method = method || 'log'
// log the subject to the console
console[method]('The subject is', subject)
// whatever we return becomes the new subject
// we don't want to change the subject so
// we return whatever was passed in
return subject
})
cy.get('button').console('info').then(($button) => {
// subject is still $button
})
})
})
context('Cypress.Cookies', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/cookies
it('.debug() - enable or disable debugging', () => {
Cypress.Cookies.debug(true)
// Cypress will now log in the console when
// cookies are set or cleared
cy.setCookie('fakeCookie', '123ABC')
cy.clearCookie('fakeCookie')
cy.setCookie('fakeCookie', '123ABC')
cy.clearCookie('fakeCookie')
cy.setCookie('fakeCookie', '123ABC')
})
})
context('Cypress.arch', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get CPU architecture name of underlying OS', () => {
// https://on.cypress.io/arch
expect(Cypress.arch).to.exist
})
})
context('Cypress.config()', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get and set configuration options', () => {
// https://on.cypress.io/config
let myConfig = Cypress.config()
expect(myConfig).to.have.property('animationDistanceThreshold', 5)
expect(myConfig).to.have.property('baseUrl', null)
expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
expect(myConfig).to.have.property('requestTimeout', 5000)
expect(myConfig).to.have.property('responseTimeout', 30000)
expect(myConfig).to.have.property('viewportHeight', 660)
expect(myConfig).to.have.property('viewportWidth', 1000)
expect(myConfig).to.have.property('pageLoadTimeout', 60000)
expect(myConfig).to.have.property('waitForAnimations', true)
expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
// this will change the config for the rest of your tests!
Cypress.config('pageLoadTimeout', 20000)
expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
Cypress.config('pageLoadTimeout', 60000)
})
})
context('Cypress.dom', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// https://on.cypress.io/dom
it('.isHidden() - determine if a DOM element is hidden', () => {
let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
let visibleP = Cypress.$('.dom-p p.visible').get(0)
// our first paragraph has css class 'hidden'
expect(Cypress.dom.isHidden(hiddenP)).to.be.true
expect(Cypress.dom.isHidden(visibleP)).to.be.false
})
})
context('Cypress.env()', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
// We can set environment variables for highly dynamic values
// https://on.cypress.io/environment-variables
it('Get environment variables', () => {
// https://on.cypress.io/env
// set multiple environment variables
Cypress.env({
host: 'veronica.dev.local',
api_server: 'http://localhost:8888/v1/',
})
// get environment variable
expect(Cypress.env('host')).to.eq('veronica.dev.local')
// set environment variable
Cypress.env('api_server', 'http://localhost:8888/v2/')
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
// get all environment variable
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
})
})
context('Cypress.log', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Control what is printed to the Command Log', () => {
// https://on.cypress.io/cypress-log
})
})
context('Cypress.platform', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get underlying OS name', () => {
// https://on.cypress.io/platform
expect(Cypress.platform).to.be.exist
})
})
context('Cypress.version', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get current version of Cypress being run', () => {
// https://on.cypress.io/version
expect(Cypress.version).to.be.exist
})
})
context('Cypress.spec', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
it('Get current spec information', () => {
// https://on.cypress.io/spec
// wrap the object so we can inspect it easily by clicking in the command log
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute'])
})
})
})
/// <reference types="cypress" />
/// JSON fixture file can be loaded directly using
// the built-in JavaScript bundler
const requiredExample = require('../../fixtures/example')
context('Files', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/files')
// load example.json fixture file and store
// in the test context object
cy.fixture('example.json').as('example')
})
it('cy.fixture() - load a fixture', () => {
// https://on.cypress.io/fixture
// Instead of writing a response inline you can
// use a fixture file's content.
// when application makes an Ajax request matching "GET **/comments/*"
// Cypress will intercept it and reply with the object in `example.json` fixture
cy.intercept('GET', '**/comments/*', { fixture: 'example.json' }).as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.wait('@getComment').its('response.body')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
})
it('cy.fixture() or require - load a fixture', function () {
// we are inside the "function () { ... }"
// callback and can use test context object "this"
// "this.example" was loaded in "beforeEach" function callback
expect(this.example, 'fixture in the test context')
.to.deep.equal(requiredExample)
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
cy.wrap(this.example)
.should('deep.equal', requiredExample)
})
it('cy.readFile() - read file contents', () => {
// https://on.cypress.io/readfile
// You can read a file and yield its contents
// The filePath is relative to your project's root.
cy.readFile(Cypress.config('configFile')).then((config) => {
expect(config).to.be.an('string')
})
})
it('cy.writeFile() - write to a file', () => {
// https://on.cypress.io/writefile
// You can write to a file
// Use a response from a request to automatically
// generate a fixture file for use later
cy.request('https://jsonplaceholder.cypress.io/users')
.then((response) => {
cy.writeFile('cypress/fixtures/users.json', response.body)
})
cy.fixture('users').should((users) => {
expect(users[0].name).to.exist
})
// JavaScript arrays and objects are stringified
// and formatted into text.
cy.writeFile('cypress/fixtures/profile.json', {
id: 8739,
name: 'Jane',
email: 'jane@example.com',
})
cy.fixture('profile').should((profile) => {
expect(profile.name).to.eq('Jane')
})
})
})
/// <reference types="cypress" />
context('Location', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/location')
})
it('cy.hash() - get the current URL hash', () => {
// https://on.cypress.io/hash
cy.hash().should('be.empty')
})
it('cy.location() - get window.location', () => {
// https://on.cypress.io/location
cy.location().should((location) => {
expect(location.hash).to.be.empty
expect(location.href).to.eq('https://example.cypress.io/commands/location')
expect(location.host).to.eq('example.cypress.io')
expect(location.hostname).to.eq('example.cypress.io')
expect(location.origin).to.eq('https://example.cypress.io')
expect(location.pathname).to.eq('/commands/location')
expect(location.port).to.eq('')
expect(location.protocol).to.eq('https:')
expect(location.search).to.be.empty
})
})
it('cy.url() - get the current URL', () => {
// https://on.cypress.io/url
cy.url().should('eq', 'https://example.cypress.io/commands/location')
})
})
/// <reference types="cypress" />
context('Misc', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/misc')
})
it('cy.exec() - execute a system command', () => {
// execute a system command.
// so you can take actions necessary for
// your test outside the scope of Cypress.
// https://on.cypress.io/exec
// we can use Cypress.platform string to
// select appropriate command
// https://on.cypress/io/platform
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
// on CircleCI Windows build machines we have a failure to run bash shell
// https://github.com/cypress-io/cypress/issues/5169
// so skip some of the tests by passing flag "--env circle=true"
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
if (isCircleOnWindows) {
cy.log('Skipping test on CircleCI')
return
}
// cy.exec problem on Shippable CI
// https://github.com/cypress-io/cypress/issues/6718
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
if (isShippable) {
cy.log('Skipping test on ShippableCI')
return
}
cy.exec('echo Jane Lane')
.its('stdout').should('contain', 'Jane Lane')
if (Cypress.platform === 'win32') {
cy.exec(`print ${Cypress.config('configFile')}`)
.its('stderr').should('be.empty')
} else {
cy.exec(`cat ${Cypress.config('configFile')}`)
.its('stderr').should('be.empty')
cy.exec('pwd')
.its('code').should('eq', 0)
}
})
it('cy.focused() - get the DOM element that has focus', () => {
// https://on.cypress.io/focused
cy.get('.misc-form').find('#name').click()
cy.focused().should('have.id', 'name')
cy.get('.misc-form').find('#description').click()
cy.focused().should('have.id', 'description')
})
context('Cypress.Screenshot', function () {
it('cy.screenshot() - take a screenshot', () => {
// https://on.cypress.io/screenshot
cy.screenshot('my-image')
})
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
Cypress.Screenshot.defaults({
blackout: ['.foo'],
capture: 'viewport',
clip: { x: 0, y: 0, width: 200, height: 200 },
scale: false,
disableTimersAndAnimations: true,
screenshotOnRunFailure: true,
onBeforeScreenshot () { },
onAfterScreenshot () { },
})
})
})
it('cy.wrap() - wrap an object', () => {
// https://on.cypress.io/wrap
cy.wrap({ foo: 'bar' })
.should('have.property', 'foo')
.and('include', 'bar')
})
})
/// <reference types="cypress" />
context('Navigation', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io')
cy.get('.navbar-nav').contains('Commands').click()
cy.get('.dropdown-menu').contains('Navigation').click()
})
it('cy.go() - go back or forward in the browser\'s history', () => {
// https://on.cypress.io/go
cy.location('pathname').should('include', 'navigation')
cy.go('back')
cy.location('pathname').should('not.include', 'navigation')
cy.go('forward')
cy.location('pathname').should('include', 'navigation')
// clicking back
cy.go(-1)
cy.location('pathname').should('not.include', 'navigation')
// clicking forward
cy.go(1)
cy.location('pathname').should('include', 'navigation')
})
it('cy.reload() - reload the page', () => {
// https://on.cypress.io/reload
cy.reload()
// reload the page without using the cache
cy.reload(true)
})
it('cy.visit() - visit a remote url', () => {
// https://on.cypress.io/visit
// Visit any sub-domain of your current domain
// Pass options to the visit
cy.visit('https://example.cypress.io/commands/navigation', {
timeout: 50000, // increase total time for the visit to resolve
onBeforeLoad (contentWindow) {
// contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true
},
onLoad (contentWindow) {
// contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true
},
})
})
})
/// <reference types="cypress" />
context('Network Requests', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/network-requests')
})
// Manage HTTP requests in your app
it('cy.request() - make an XHR request', () => {
// https://on.cypress.io/request
cy.request('https://jsonplaceholder.cypress.io/comments')
.should((response) => {
expect(response.status).to.eq(200)
// the server sometimes gets an extra comment posted from another machine
// which gets returned as 1 extra object
expect(response.body).to.have.property('length').and.be.oneOf([500, 501])
expect(response).to.have.property('headers')
expect(response).to.have.property('duration')
})
})
it('cy.request() - verify response using BDD syntax', () => {
cy.request('https://jsonplaceholder.cypress.io/comments')
.then((response) => {
// https://on.cypress.io/assertions
expect(response).property('status').to.equal(200)
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501])
expect(response).to.include.keys('headers', 'duration')
})
})
it('cy.request() with query parameters', () => {
// will execute request
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
cy.request({
url: 'https://jsonplaceholder.cypress.io/comments',
qs: {
postId: 1,
id: 3,
},
})
.its('body')
.should('be.an', 'array')
.and('have.length', 1)
.its('0') // yields first element of the array
.should('contain', {
postId: 1,
id: 3,
})
})
it('cy.request() - pass result to the second request', () => {
// first, let's find out the userId of the first user we have
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
.its('body') // yields the response object
.its('0') // yields the first element of the returned list
// the above two commands its('body').its('0')
// can be written as its('body.0')
// if you do not care about TypeScript checks
.then((user) => {
expect(user).property('id').to.be.a('number')
// make a new post on behalf of the user
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
userId: user.id,
title: 'Cypress Test Runner',
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
})
})
// note that the value here is the returned value of the 2nd request
// which is the new post object
.then((response) => {
expect(response).property('status').to.equal(201) // new entity created
expect(response).property('body').to.contain({
title: 'Cypress Test Runner',
})
// we don't know the exact post id - only that it will be > 100
// since JSONPlaceholder has built-in 100 posts
expect(response.body).property('id').to.be.a('number')
.and.to.be.gt(100)
// we don't know the user id here - since it was in above closure
// so in this test just confirm that the property is there
expect(response.body).property('userId').to.be.a('number')
})
})
it('cy.request() - save response in the shared test context', () => {
// https://on.cypress.io/variables-and-aliases
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
.its('body').its('0') // yields the first element of the returned list
.as('user') // saves the object in the test context
.then(function () {
// NOTE 👀
// By the time this callback runs the "as('user')" command
// has saved the user object in the test context.
// To access the test context we need to use
// the "function () { ... }" callback form,
// otherwise "this" points at a wrong or undefined object!
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
userId: this.user.id,
title: 'Cypress Test Runner',
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
})
.its('body').as('post') // save the new post from the response
})
.then(function () {
// When this callback runs, both "cy.request" API commands have finished
// and the test context has "user" and "post" objects set.
// Let's verify them.
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
})
})
it('cy.intercept() - route responses to matching requests', () => {
// https://on.cypress.io/intercept
let message = 'whoa, this comment does not exist'
// Listen to GET to comments/1
cy.intercept('GET', '**/comments/*').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// https://on.cypress.io/wait
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
// Listen to POST to comments
cy.intercept('POST', '**/comments').as('postComment')
// we have code that posts a comment when
// the button is clicked in scripts.js
cy.get('.network-post').click()
cy.wait('@postComment').should(({ request, response }) => {
expect(request.body).to.include('email')
expect(request.headers).to.have.property('content-type')
expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()')
})
// Stub a response to PUT comments/ ****
cy.intercept({
method: 'PUT',
url: '**/comments/*',
}, {
statusCode: 404,
body: { error: message },
headers: { 'access-control-allow-origin': '*' },
delayMs: 500,
}).as('putComment')
// we have code that puts a comment when
// the button is clicked in scripts.js
cy.get('.network-put').click()
cy.wait('@putComment')
// our 404 statusCode logic in scripts.js executed
cy.get('.network-put-comment').should('contain', message)
})
})
/// <reference types="cypress" />
context('Querying', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/querying')
})
// The most commonly used query is 'cy.get()', you can
// think of this like the '$' in jQuery
it('cy.get() - query DOM elements', () => {
// https://on.cypress.io/get
cy.get('#query-btn').should('contain', 'Button')
cy.get('.query-btn').should('contain', 'Button')
cy.get('#querying .well>button:first').should('contain', 'Button')
// ↲
// Use CSS selectors just like jQuery
cy.get('[data-test-id="test-example"]').should('have.class', 'example')
// 'cy.get()' yields jQuery object, you can get its attribute
// by invoking `.attr()` method
cy.get('[data-test-id="test-example"]')
.invoke('attr', 'data-test-id')
.should('equal', 'test-example')
// or you can get element's CSS property
cy.get('[data-test-id="test-example"]')
.invoke('css', 'position')
.should('equal', 'static')
// or use assertions directly during 'cy.get()'
// https://on.cypress.io/assertions
cy.get('[data-test-id="test-example"]')
.should('have.attr', 'data-test-id', 'test-example')
.and('have.css', 'position', 'static')
})
it('cy.contains() - query DOM elements with matching content', () => {
// https://on.cypress.io/contains
cy.get('.query-list')
.contains('bananas')
.should('have.class', 'third')
// we can pass a regexp to `.contains()`
cy.get('.query-list')
.contains(/^b\w+/)
.should('have.class', 'third')
cy.get('.query-list')
.contains('apples')
.should('have.class', 'first')
// passing a selector to contains will
// yield the selector containing the text
cy.get('#querying')
.contains('ul', 'oranges')
.should('have.class', 'query-list')
cy.get('.query-button')
.contains('Save Form')
.should('have.class', 'btn')
})
it('.within() - query DOM elements within a specific element', () => {
// https://on.cypress.io/within
cy.get('.query-form').within(() => {
cy.get('input:first').should('have.attr', 'placeholder', 'Email')
cy.get('input:last').should('have.attr', 'placeholder', 'Password')
})
})
it('cy.root() - query the root DOM element', () => {
// https://on.cypress.io/root
// By default, root is the document
cy.root().should('match', 'html')
cy.get('.query-ul').within(() => {
// In this within, the root is now the ul DOM element
cy.root().should('have.class', 'query-ul')
})
})
it('best practices - selecting elements', () => {
// https://on.cypress.io/best-practices#Selecting-Elements
cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
// Worst - too generic, no context
cy.get('button').click()
// Bad. Coupled to styling. Highly subject to change.
cy.get('.btn.btn-large').click()
// Average. Coupled to the `name` attribute which has HTML semantics.
cy.get('[name=submission]').click()
// Better. But still coupled to styling or JS event listeners.
cy.get('#main').click()
// Slightly better. Uses an ID but also ensures the element
// has an ARIA role attribute
cy.get('#main[role=button]').click()
// Much better. But still coupled to text content that may change.
cy.contains('Submit').click()
// Best. Insulated from all changes.
cy.get('[data-cy=submit]').click()
})
})
})
/// <reference types="cypress" />
context('Spies, Stubs, and Clock', () => {
it('cy.spy() - wrap a method in a spy', () => {
// https://on.cypress.io/spy
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
const obj = {
foo () {},
}
const spy = cy.spy(obj, 'foo').as('anyArgs')
obj.foo()
expect(spy).to.be.called
})
it('cy.spy() retries until assertions pass', () => {
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
const obj = {
/**
* Prints the argument passed
* @param x {any}
*/
foo (x) {
console.log('obj.foo called with', x)
},
}
cy.spy(obj, 'foo').as('foo')
setTimeout(() => {
obj.foo('first')
}, 500)
setTimeout(() => {
obj.foo('second')
}, 2500)
cy.get('@foo').should('have.been.calledTwice')
})
it('cy.stub() - create a stub and/or replace a function with stub', () => {
// https://on.cypress.io/stub
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
const obj = {
/**
* prints both arguments to the console
* @param a {string}
* @param b {string}
*/
foo (a, b) {
console.log('a', a, 'b', b)
},
}
const stub = cy.stub(obj, 'foo').as('foo')
obj.foo('foo', 'bar')
expect(stub).to.be.called
})
it('cy.clock() - control time in the browser', () => {
// https://on.cypress.io/clock
// create the date in UTC so it's always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
cy.clock(now)
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.get('#clock-div').click()
cy.get('#clock-div')
.should('have.text', '1489449600')
})
it('cy.tick() - move time in the browser', () => {
// https://on.cypress.io/tick
// create the date in UTC so it's always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
cy.clock(now)
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.get('#tick-div').click()
cy.get('#tick-div')
.should('have.text', '1489449600')
cy.tick(10000) // 10 seconds passed
cy.get('#tick-div').click()
cy.get('#tick-div')
.should('have.text', '1489449610')
})
it('cy.stub() matches depending on arguments', () => {
// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const greeter = {
/**
* Greets a person
* @param {string} name
*/
greet (name) {
return `Hello, ${name}!`
},
}
cy.stub(greeter, 'greet')
.callThrough() // if you want non-matched calls to call the real method
.withArgs(Cypress.sinon.match.string).returns('Hi')
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name'))
expect(greeter.greet('World')).to.equal('Hi')
expect(() => greeter.greet(42)).to.throw('Invalid name')
expect(greeter.greet).to.have.been.calledTwice
// non-matched calls goes the actual method
expect(greeter.greet()).to.equal('Hello, undefined!')
})
it('matches call arguments using Sinon matchers', () => {
// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const calculator = {
/**
* returns the sum of two arguments
* @param a {number}
* @param b {number}
*/
add (a, b) {
return a + b
},
}
const spy = cy.spy(calculator, 'add').as('add')
expect(calculator.add(2, 3)).to.equal(5)
// if we want to assert the exact values used during the call
expect(spy).to.be.calledWith(2, 3)
// let's confirm "add" method was called with two numbers
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number)
// alternatively, provide the value to match
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3))
// match any value
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)
// match any value from a list
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3)
/**
* Returns true if the given number is even
* @param {number} x
*/
const isEven = (x) => x % 2 === 0
// expect the value to pass a custom predicate function
// the second argument to "sinon.match(predicate, message)" is
// shown if the predicate does not pass and assertion fails
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3)
/**
* Returns a function that checks if a given number is larger than the limit
* @param {number} limit
* @returns {(x: number) => boolean}
*/
const isGreaterThan = (limit) => (x) => x > limit
/**
* Returns a function that checks if a given number is less than the limit
* @param {number} limit
* @returns {(x: number) => boolean}
*/
const isLessThan = (limit) => (x) => x < limit
// you can combine several matchers using "and", "or"
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')),
)
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)),
)
// matchers can be used from BDD assertions
cy.get('@add').should('have.been.calledWith',
Cypress.sinon.match.number, Cypress.sinon.match(3))
// you can alias matchers for shorter test code
const { match: M } = Cypress.sinon
cy.get('@add').should('have.been.calledWith', M.number, M(3))
})
})
/// <reference types="cypress" />
context('Local Storage / Session Storage', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/storage')
})
// Although localStorage is automatically cleared
// in between tests to maintain a clean state
// sometimes we need to clear localStorage manually
it('cy.clearLocalStorage() - clear all data in localStorage for the current origin', () => {
// https://on.cypress.io/clearlocalstorage
cy.get('.ls-btn').click()
cy.get('.ls-btn').should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
cy.clearLocalStorage()
cy.getAllLocalStorage().should(() => {
expect(localStorage.getItem('prop1')).to.be.null
expect(localStorage.getItem('prop2')).to.be.null
expect(localStorage.getItem('prop3')).to.be.null
})
cy.get('.ls-btn').click()
cy.get('.ls-btn').should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
// Clear key matching string in localStorage
cy.clearLocalStorage('prop1')
cy.getAllLocalStorage().should(() => {
expect(localStorage.getItem('prop1')).to.be.null
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
cy.get('.ls-btn').click()
cy.get('.ls-btn').should(() => {
expect(localStorage.getItem('prop1')).to.eq('red')
expect(localStorage.getItem('prop2')).to.eq('blue')
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
// Clear keys matching regex in localStorage
cy.clearLocalStorage(/prop1|2/)
cy.getAllLocalStorage().should(() => {
expect(localStorage.getItem('prop1')).to.be.null
expect(localStorage.getItem('prop2')).to.be.null
expect(localStorage.getItem('prop3')).to.eq('magenta')
})
})
it('cy.getAllLocalStorage() - get all data in localStorage for all origins', () => {
// https://on.cypress.io/getalllocalstorage
cy.get('.ls-btn').click()
// getAllLocalStorage() yields a map of origins to localStorage values
cy.getAllLocalStorage().should((storageMap) => {
expect(storageMap).to.deep.equal({
// other origins will also be present if localStorage is set on them
'https://example.cypress.io': {
'prop1': 'red',
'prop2': 'blue',
'prop3': 'magenta',
},
})
})
})
it('cy.clearAllLocalStorage() - clear all data in localStorage for all origins', () => {
// https://on.cypress.io/clearalllocalstorage
cy.get('.ls-btn').click()
// clearAllLocalStorage() yields null
cy.clearAllLocalStorage()
cy.getAllLocalStorage().should(() => {
expect(localStorage.getItem('prop1')).to.be.null
expect(localStorage.getItem('prop2')).to.be.null
expect(localStorage.getItem('prop3')).to.be.null
})
})
it('cy.getAllSessionStorage() - get all data in sessionStorage for all origins', () => {
// https://on.cypress.io/getallsessionstorage
cy.get('.ls-btn').click()
// getAllSessionStorage() yields a map of origins to sessionStorage values
cy.getAllSessionStorage().should((storageMap) => {
expect(storageMap).to.deep.equal({
// other origins will also be present if sessionStorage is set on them
'https://example.cypress.io': {
'prop4': 'cyan',
'prop5': 'yellow',
'prop6': 'black',
},
})
})
})
it('cy.clearAllSessionStorage() - clear all data in sessionStorage for all origins', () => {
// https://on.cypress.io/clearallsessionstorage
cy.get('.ls-btn').click()
// clearAllSessionStorage() yields null
cy.clearAllSessionStorage()
cy.getAllSessionStorage().should(() => {
expect(sessionStorage.getItem('prop4')).to.be.null
expect(sessionStorage.getItem('prop5')).to.be.null
expect(sessionStorage.getItem('prop6')).to.be.null
})
})
})
/// <reference types="cypress" />
context('Traversal', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/traversal')
})
it('.children() - get child DOM elements', () => {
// https://on.cypress.io/children
cy.get('.traversal-breadcrumb')
.children('.active')
.should('contain', 'Data')
})
it('.closest() - get closest ancestor DOM element', () => {
// https://on.cypress.io/closest
cy.get('.traversal-badge')
.closest('ul')
.should('have.class', 'list-group')
})
it('.eq() - get a DOM element at a specific index', () => {
// https://on.cypress.io/eq
cy.get('.traversal-list>li')
.eq(1).should('contain', 'siamese')
})
it('.filter() - get DOM elements that match the selector', () => {
// https://on.cypress.io/filter
cy.get('.traversal-nav>li')
.filter('.active').should('contain', 'About')
})
it('.find() - get descendant DOM elements of the selector', () => {
// https://on.cypress.io/find
cy.get('.traversal-pagination')
.find('li').find('a')
.should('have.length', 7)
})
it('.first() - get first DOM element', () => {
// https://on.cypress.io/first
cy.get('.traversal-table td')
.first().should('contain', '1')
})
it('.last() - get last DOM element', () => {
// https://on.cypress.io/last
cy.get('.traversal-buttons .btn')
.last().should('contain', 'Submit')
})
it('.next() - get next sibling DOM element', () => {
// https://on.cypress.io/next
cy.get('.traversal-ul')
.contains('apples').next().should('contain', 'oranges')
})
it('.nextAll() - get all next sibling DOM elements', () => {
// https://on.cypress.io/nextall
cy.get('.traversal-next-all')
.contains('oranges')
.nextAll().should('have.length', 3)
})
it('.nextUntil() - get next sibling DOM elements until next el', () => {
// https://on.cypress.io/nextuntil
cy.get('#veggies')
.nextUntil('#nuts').should('have.length', 3)
})
it('.not() - remove DOM elements from set of DOM elements', () => {
// https://on.cypress.io/not
cy.get('.traversal-disabled .btn')
.not('[disabled]').should('not.contain', 'Disabled')
})
it('.parent() - get parent DOM element from DOM elements', () => {
// https://on.cypress.io/parent
cy.get('.traversal-mark')
.parent().should('contain', 'Morbi leo risus')
})
it('.parents() - get parent DOM elements from DOM elements', () => {
// https://on.cypress.io/parents
cy.get('.traversal-cite')
.parents().should('match', 'blockquote')
})
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
// https://on.cypress.io/parentsuntil
cy.get('.clothes-nav')
.find('.active')
.parentsUntil('.clothes-nav')
.should('have.length', 2)
})
it('.prev() - get previous sibling DOM element', () => {
// https://on.cypress.io/prev
cy.get('.birds').find('.active')
.prev().should('contain', 'Lorikeets')
})
it('.prevAll() - get all previous sibling DOM elements', () => {
// https://on.cypress.io/prevall
cy.get('.fruits-list').find('.third')
.prevAll().should('have.length', 2)
})
it('.prevUntil() - get all previous sibling DOM elements until el', () => {
// https://on.cypress.io/prevuntil
cy.get('.foods-list').find('#nuts')
.prevUntil('#veggies').should('have.length', 3)
})
it('.siblings() - get all sibling DOM elements', () => {
// https://on.cypress.io/siblings
cy.get('.traversal-pills .active')
.siblings().should('have.length', 2)
})
})
/// <reference types="cypress" />
context('Utilities', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/utilities')
})
it('Cypress._ - call a lodash method', () => {
// https://on.cypress.io/_
cy.request('https://jsonplaceholder.cypress.io/users')
.then((response) => {
let ids = Cypress._.chain(response.body).map('id').take(3).value()
expect(ids).to.deep.eq([1, 2, 3])
})
})
it('Cypress.$ - call a jQuery method', () => {
// https://on.cypress.io/$
let $li = Cypress.$('.utility-jquery li:first')
cy.wrap($li).should('not.have.class', 'active')
cy.wrap($li).click()
cy.wrap($li).should('have.class', 'active')
})
it('Cypress.Blob - blob utilities and base64 string conversion', () => {
// https://on.cypress.io/blob
cy.get('.utility-blob').then(($div) => {
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
// get the dataUrl string for the javascript-logo
return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous')
.then((dataUrl) => {
// create an <img> element and set its src to the dataUrl
let img = Cypress.$('<img />', { src: dataUrl })
// need to explicitly return cy here since we are initially returning
// the Cypress.Blob.imgSrcToDataURL promise to our test
// append the image
$div.append(img)
cy.get('.utility-blob img').click()
cy.get('.utility-blob img').should('have.attr', 'src', dataUrl)
})
})
})
it('Cypress.minimatch - test out glob patterns against strings', () => {
// https://on.cypress.io/minimatch
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', {
matchBase: true,
})
expect(matching, 'matching wildcard').to.be.true
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', {
matchBase: true,
})
expect(matching, 'comments').to.be.false
// ** matches against all downstream path segments
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', {
matchBase: true,
})
expect(matching, 'comments').to.be.true
// whereas * matches only the next path segment
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', {
matchBase: false,
})
expect(matching, 'comments').to.be.false
})
it('Cypress.Promise - instantiate a bluebird promise', () => {
// https://on.cypress.io/promise
let waited = false
/**
* @return Bluebird<string>
*/
function waitOneSecond () {
// return a promise that resolves after 1 second
return new Cypress.Promise((resolve, reject) => {
setTimeout(() => {
// set waited to true
waited = true
// resolve with 'foo' string
resolve('foo')
}, 1000)
})
}
cy.then(() => {
// return a promise to cy.then() that
// is awaited until it resolves
return waitOneSecond().then((str) => {
expect(str).to.eq('foo')
expect(waited).to.be.true
})
})
})
})
/// <reference types="cypress" />
context('Viewport', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/viewport')
})
it('cy.viewport() - set the viewport size and dimension', () => {
// https://on.cypress.io/viewport
cy.get('#navbar').should('be.visible')
cy.viewport(320, 480)
// the navbar should have collapse since our screen is smaller
cy.get('#navbar').should('not.be.visible')
cy.get('.navbar-toggle').should('be.visible').click()
cy.get('.nav').find('a').should('be.visible')
// lets see what our app looks like on a super large screen
cy.viewport(2999, 2999)
// cy.viewport() accepts a set of preset sizes
// to easily set the screen to a device's width and height
// We added a cy.wait() between each viewport change so you can see
// the change otherwise it is a little too fast to see :)
cy.viewport('macbook-15')
cy.wait(200)
cy.viewport('macbook-13')
cy.wait(200)
cy.viewport('macbook-11')
cy.wait(200)
cy.viewport('ipad-2')
cy.wait(200)
cy.viewport('ipad-mini')
cy.wait(200)
cy.viewport('iphone-6+')
cy.wait(200)
cy.viewport('iphone-6')
cy.wait(200)
cy.viewport('iphone-5')
cy.wait(200)
cy.viewport('iphone-4')
cy.wait(200)
cy.viewport('iphone-3')
cy.wait(200)
// cy.viewport() accepts an orientation for all presets
// the default orientation is 'portrait'
cy.viewport('ipad-2', 'portrait')
cy.wait(200)
cy.viewport('iphone-4', 'landscape')
cy.wait(200)
// The viewport will be reset back to the default dimensions
// in between tests (the default can be set in cypress.config.{js|ts})
})
})
/// <reference types="cypress" />
context('Waiting', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/waiting')
})
// BE CAREFUL of adding unnecessary wait times.
// https://on.cypress.io/best-practices#Unnecessary-Waiting
// https://on.cypress.io/wait
it('cy.wait() - wait for a specific amount of time', () => {
cy.get('.wait-input1').type('Wait 1000ms after typing')
cy.wait(1000)
cy.get('.wait-input2').type('Wait 1000ms after typing')
cy.wait(1000)
cy.get('.wait-input3').type('Wait 1000ms after typing')
cy.wait(1000)
})
it('cy.wait() - wait for a specific route', () => {
// Listen to GET to comments/1
cy.intercept('GET', '**/comments/*').as('getComment')
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.network-btn').click()
// wait for GET comments/1
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
})
})
/// <reference types="cypress" />
context('Window', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/window')
})
it('cy.window() - get the global window object', () => {
// https://on.cypress.io/window
cy.window().should('have.property', 'top')
})
it('cy.document() - get the document object', () => {
// https://on.cypress.io/document
cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
})
it('cy.title() - get the title', () => {
// https://on.cypress.io/title
cy.title().should('include', 'Kitchen Sink')
})
})
describe('template spec', () => {
it('passes', () => {
cy.visit('https://example.cypress.io')
})
/* ==== Test Created with Cypress Studio ==== */
it('test1', function() {
it('test', function() {
/* ==== Generated with Cypress Studio ==== */
cy.visit('http://localhost:3000/');
cy.get('[data-testid="login-username-input"]').clear('z');
......@@ -14,24 +10,10 @@ describe('template spec', () => {
cy.get('[data-testid="login-password-input"]').type('9%#F46vt');
cy.get('[data-testid="login-captcha-input"]').clear('8');
cy.get('[data-testid="login-captcha-input"]').type('8888');
cy.get('.el-checkbox__label').click();
cy.get('.el-checkbox__original').check();
cy.get('[data-testid="login-submit-button"]').click();
cy.get('[data-testid="menu-item-collectorList"]').click();
cy.get('[data-testid="menu-item-monitor"] > span').click();
cy.get('[data-testid="menu-item-dust-overview"] > span').click();
cy.get('[data-testid="dust-add-button"] > span').click();
cy.get('#el-id-7502-285').click();
cy.get('#el-id-7502-97 > span').click();
cy.get('#el-id-7502-286').clear('1');
cy.get('#el-id-7502-286').type('1');
cy.get('#el-id-7502-287').clear('2');
cy.get('#el-id-7502-287').type('2');
cy.get('#el-id-7502-289').click();
cy.get('.el-dialog__headerbtn > .el-icon > svg').click();
cy.get('[data-testid="dust-search-button"] > span').click();
cy.get(':nth-child(1) > .el-table_1_column_10 > .cell > [data-testid="dust-view-button"]').click();
cy.get('[data-testid="dust-monitoring-status-matrix"] > .left > :nth-child(1) > :nth-child(1)').click();
cy.get('.el-select__suffix > .el-icon > svg').click();
cy.get('#el-id-7502-313 > span').click();
/* ==== End Cypress Studio ==== */
});
})
\ No newline at end of file
......@@ -7,8 +7,10 @@
"dev": "vite --mode development",
"build": "vite build",
"preview": "vite preview",
"cy:open": "cypress open",
"cy:open:studio": "cypress open --config experimentalStudio=true",
"cy:open": "vite --mode development && cypress open",
"cy:open:studio": "./start-studio.sh",
"studio": "./start-studio.sh",
"studio:help": "./start-studio.sh --help",
"cy:run": "cypress run",
"cy:run:studio": "cypress run --spec 'cypress/e2e/studio-generated/*.cy.js'",
"cy:run:ci": "cypress run --browser chrome --headless",
......
#!/bin/bash
# DC-TOM Cypress Studio 启动脚本
# 功能: 先启动开发服务器,然后启动 Cypress Studio
set -e # 遇到错误时退出
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}ℹ️ $1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
# 检查端口是否被占用
check_port() {
local port=$1
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1; then
return 0 # 端口被占用
else
return 1 # 端口未被占用
fi
}
# 等待开发服务器启动
wait_for_dev_server() {
local max_attempts=30
local attempt=1
log_info "等待开发服务器启动 (localhost:3000)..."
while [ $attempt -le $max_attempts ]; do
if curl -s http://localhost:3000 > /dev/null 2>&1; then
log_success "开发服务器已启动并运行在 http://localhost:3000"
return 0
fi
echo -n "."
sleep 1
attempt=$((attempt + 1))
done
log_error "开发服务器启动超时"
return 1
}
# 启动开发服务器
start_dev_server() {
if check_port 3000; then
log_warning "端口 3000 已被占用,跳过开发服务器启动"
log_info "如果需要重新启动,请先停止现有的服务"
return 0
fi
log_info "启动开发服务器..."
npm run dev &
DEV_PID=$!
# 等待服务器启动
if wait_for_dev_server; then
log_success "开发服务器启动成功 (PID: $DEV_PID)"
return 0
else
log_error "开发服务器启动失败"
if [ ! -z "$DEV_PID" ]; then
kill $DEV_PID 2>/dev/null || true
fi
return 1
fi
}
# 启动 Cypress Studio
start_cypress_studio() {
log_info "启动 Cypress Studio..."
log_info "Cypress Studio 将在新窗口中打开"
log_info "您可以在 Studio 中录制和编辑测试用例"
# 启动 Cypress Studio
npx cypress open --config experimentalStudio=true
}
# 清理函数
cleanup() {
log_info "正在清理进程..."
if [ ! -z "$DEV_PID" ] && kill -0 $DEV_PID 2>/dev/null; then
log_info "停止开发服务器 (PID: $DEV_PID)"
kill $DEV_PID 2>/dev/null || true
fi
}
# 设置退出处理
trap cleanup EXIT INT TERM
# 主函数
main() {
echo
log_info "🚀 DC-TOM Cypress Studio 启动器"
echo
# 检查依赖
if ! command -v npm >/dev/null 2>&1; then
log_error "npm 未安装或不在 PATH 中"
exit 1
fi
if ! command -v npx >/dev/null 2>&1; then
log_error "npx 未安装或不在 PATH 中"
exit 1
fi
# 启动开发服务器
if start_dev_server; then
echo
log_success "开发环境准备就绪"
echo
# 启动 Cypress Studio
start_cypress_studio
else
log_error "无法启动开发服务器,Cypress Studio 启动已取消"
exit 1
fi
}
# 显示使用说明
show_usage() {
echo "用法: $0"
echo ""
echo "功能:"
echo " 1. 自动启动开发服务器 (localhost:3000)"
echo " 2. 等待服务器就绪"
echo " 3. 启动 Cypress Studio"
echo ""
echo "注意:"
echo " - 如果端口 3000 已被占用,将跳过开发服务器启动"
echo " - 关闭 Cypress Studio 时,开发服务器也会自动停止"
echo " - 使用 Ctrl+C 可以同时停止两个服务"
}
# 处理命令行参数
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
show_usage
exit 0
fi
# 运行主函数
main "$@"
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment