feat: 添加测试框架和覆盖率报告功能
feat(测试): 新增Playwright和Vitest测试配置 feat(测试): 添加测试覆盖率报告生成功能 feat(测试): 实现前后端测试脚本集成 fix(测试): 修复测试密码不匹配问题 fix(测试): 修正URL等待策略 fix(测试): 调整错误消息选择器 refactor(测试): 重构测试目录结构 refactor(测试): 优化测试用例组织方式 docs: 更新测试报告文档 docs: 添加测试覆盖率报告模板 ci: 添加Docker测试环境配置 ci: 实现测试自动化脚本 chore: 更新依赖版本 chore: 添加测试相关配置文件
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# 测试环境配置示例
|
||||
# 复制此文件为 .env 并根据实际情况修改配置
|
||||
|
||||
# 测试基础URL
|
||||
TEST_BASE_URL=http://localhost:3001
|
||||
|
||||
# Playwright配置
|
||||
PLAYWRIGHT_HEADLESS=false
|
||||
|
||||
# 前端配置
|
||||
VITE_BASE_URL=http://localhost:3001
|
||||
|
||||
# CI/CD环境配置
|
||||
CI=false
|
||||
|
||||
# 测试数据库配置(可选)
|
||||
TEST_DB_HOST=localhost
|
||||
TEST_DB_PORT=5432
|
||||
TEST_DB_NAME=novalon_manage_test
|
||||
TEST_DB_USER=test
|
||||
TEST_DB_PASSWORD=test
|
||||
|
||||
# 测试超时配置(可选)
|
||||
TEST_TIMEOUT=120000
|
||||
TEST_ACTION_TIMEOUT=30000
|
||||
TEST_NAVIGATION_TIMEOUT=60000
|
||||
|
||||
# 测试重试配置(可选)
|
||||
TEST_RETRIES=3
|
||||
|
||||
# 测试并行度配置(可选)
|
||||
TEST_WORKERS=4
|
||||
|
||||
# 测试报告配置(可选)
|
||||
TEST_REPORT_FOLDER=playwright-report
|
||||
TEST_RESULTS_FOLDER=test-results
|
||||
@@ -0,0 +1,29 @@
|
||||
FROM mcr.microsoft.com/playwright:v1.58.2-jammy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装依赖
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# 复制测试文件
|
||||
COPY e2e ./e2e
|
||||
COPY playwright.config.ts ./
|
||||
COPY tsconfig.json ./
|
||||
|
||||
# 创建测试结果目录
|
||||
RUN mkdir -p /app/test-results /app/playwright-report
|
||||
|
||||
# 安装Playwright浏览器
|
||||
RUN npx playwright install --with-deps chromium
|
||||
|
||||
# 设置环境变量
|
||||
ENV CI=true
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||
|
||||
# 运行测试
|
||||
CMD ["npx", "playwright", "test", "--reporter=json", "--reporter=html", "--reporter=junit"]
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
||||
CMD test -f /app/playwright-report/index.html || exit 1
|
||||
@@ -1,342 +0,0 @@
|
||||
# E2E测试指南
|
||||
|
||||
## 概述
|
||||
|
||||
本项目使用Playwright进行端到端(E2E)测试,覆盖关键用户流程和业务场景。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **测试框架**: Playwright
|
||||
- **语言**: TypeScript
|
||||
- **浏览器**: Chromium
|
||||
- **模式**: Page Object Model (POM)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
novalon-manage-web/e2e/
|
||||
├── pages/ # Page Object Model
|
||||
│ ├── LoginPage.ts # 登录页面
|
||||
│ ├── DashboardPage.ts # 仪表板页面
|
||||
│ ├── UserManagementPage.ts # 用户管理页面
|
||||
│ └── RoleManagementPage.ts # 角色管理页面
|
||||
├── fixtures/ # 测试数据fixtures
|
||||
│ └── test-data.ts # 测试数据生成器
|
||||
├── utils/ # 工具类
|
||||
│ └── api-client.ts # API客户端
|
||||
├── auth.spec.ts # 认证测试
|
||||
├── user-management.spec.ts # 用户管理测试
|
||||
├── role-management.spec.ts # 角色管理测试
|
||||
├── system-config.spec.ts # 系统配置测试
|
||||
├── basic.spec.ts # 基础功能测试
|
||||
└── complete-workflow.spec.ts # 完整业务流程测试
|
||||
```
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. **启动后端服务**:
|
||||
```bash
|
||||
cd novalon-manage-api
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
2. **启动前端服务**:
|
||||
```bash
|
||||
cd novalon-manage-web
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. **确保数据库连接正常**
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
cd novalon-manage-web
|
||||
npm install
|
||||
npx playwright install --with-deps chromium
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 运行所有E2E测试
|
||||
|
||||
```bash
|
||||
cd novalon-manage-web
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
### 运行特定测试文件
|
||||
|
||||
```bash
|
||||
npx playwright test auth.spec.ts
|
||||
```
|
||||
|
||||
### 运行特定测试用例
|
||||
|
||||
```bash
|
||||
npx playwright test -g "成功登录流程"
|
||||
```
|
||||
|
||||
### 调试模式
|
||||
|
||||
```bash
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
### 有头模式(显示浏览器)
|
||||
|
||||
```bash
|
||||
npx playwright test --headed
|
||||
```
|
||||
|
||||
### 查看测试报告
|
||||
|
||||
```bash
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
## 测试覆盖范围
|
||||
|
||||
### 1. 认证测试 (auth.spec.ts)
|
||||
- ✅ 成功登录流程
|
||||
- ✅ 登录失败 - 无效凭证
|
||||
- ✅ 登录失败 - 缺少必填字段
|
||||
- ✅ 登出流程
|
||||
- ✅ 登录后可以访问所有菜单
|
||||
|
||||
### 2. 用户管理测试 (user-management.spec.ts)
|
||||
- ✅ 创建用户完整流程
|
||||
- ✅ 编辑用户流程
|
||||
- ✅ 删除用户流程
|
||||
- ✅ 搜索用户功能
|
||||
- ✅ 分页功能
|
||||
- ✅ 批量删除用户
|
||||
- ✅ 用户状态切换
|
||||
- ✅ 导出用户数据
|
||||
|
||||
### 3. 角色管理测试 (role-management.spec.ts)
|
||||
- ✅ 创建角色完整流程
|
||||
- ✅ 编辑角色流程
|
||||
- ✅ 分配权限流程
|
||||
- ✅ 删除角色流程
|
||||
- ✅ 角色状态切换
|
||||
- ✅ 搜索角色功能
|
||||
- ✅ 批量删除角色
|
||||
- ✅ 复制角色
|
||||
|
||||
### 4. 系统配置测试 (system-config.spec.ts)
|
||||
- ✅ 查看系统配置
|
||||
- ✅ 编辑系统配置
|
||||
- ✅ 搜索配置项
|
||||
|
||||
### 5. 完整业务流程测试 (complete-workflow.spec.ts)
|
||||
- ✅ 完整用户管理流程
|
||||
- ✅ 完整菜单管理流程
|
||||
- ✅ 完整系统配置流程
|
||||
- ✅ 完整权限控制流程
|
||||
|
||||
### 6. 基础功能测试 (basic.spec.ts)
|
||||
- ✅ 首页加载测试
|
||||
- ✅ 登录页面访问测试
|
||||
- ✅ 后端健康检查
|
||||
- ✅ 数据库连接检查
|
||||
- ✅ 前端页面可访问性
|
||||
- ✅ API代理配置验证
|
||||
|
||||
## Page Object Model
|
||||
|
||||
### LoginPage
|
||||
|
||||
```typescript
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
```
|
||||
|
||||
### DashboardPage
|
||||
|
||||
```typescript
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
```
|
||||
|
||||
### UserManagementPage
|
||||
|
||||
```typescript
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
|
||||
const userPage = new UserManagementPage(page);
|
||||
await userPage.clickCreateUser();
|
||||
await userPage.fillUserForm(userData);
|
||||
await userPage.submitForm();
|
||||
```
|
||||
|
||||
### RoleManagementPage
|
||||
|
||||
```typescript
|
||||
import { RoleManagementPage } from './pages/RoleManagementPage';
|
||||
|
||||
const rolePage = new RoleManagementPage(page);
|
||||
await rolePage.clickCreateRole();
|
||||
await rolePage.fillRoleForm(roleData);
|
||||
await rolePage.submitForm();
|
||||
```
|
||||
|
||||
## 测试数据Fixtures
|
||||
|
||||
### 使用预定义测试数据
|
||||
|
||||
```typescript
|
||||
import { test } from './fixtures/test-data';
|
||||
|
||||
test('使用admin用户', async ({ adminUser }) => {
|
||||
console.log(adminUser.username); // 'admin'
|
||||
console.log(adminUser.password); // 'admin123'
|
||||
});
|
||||
```
|
||||
|
||||
### 动态生成测试数据
|
||||
|
||||
```typescript
|
||||
import { test } from './fixtures/test-data';
|
||||
|
||||
test('生成测试用户', async ({ generateTestUser }) => {
|
||||
const user = generateTestUser();
|
||||
console.log(user.username); // 'testuser_1234567890'
|
||||
console.log(user.email); // 'test_1234567890@example.com'
|
||||
});
|
||||
```
|
||||
|
||||
## CI/CD集成
|
||||
|
||||
E2E测试已集成到Woodpecker CI流水线中:
|
||||
|
||||
```yaml
|
||||
frontend-e2e-test:
|
||||
image: mcr.microsoft.com/playwright:v1.42.0-jammy
|
||||
commands:
|
||||
- cd novalon-manage-web
|
||||
- npm ci
|
||||
- npx playwright install --with-deps chromium
|
||||
- npx playwright test
|
||||
environment:
|
||||
NODE_ENV: test
|
||||
CI: true
|
||||
depends_on:
|
||||
- deploy-staging
|
||||
when:
|
||||
- event: pull_request
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用Page Object Model
|
||||
- 将页面逻辑封装在Page类中
|
||||
- 避免在测试文件中直接操作DOM元素
|
||||
- 提高测试可维护性
|
||||
|
||||
### 2. 使用稳定的定位器
|
||||
```typescript
|
||||
// ❌ 不推荐:使用CSS类名
|
||||
await page.click('.btn-primary');
|
||||
|
||||
// ✅ 推荐:使用角色定位器
|
||||
await page.getByRole('button', { name: '提交' }).click();
|
||||
|
||||
// ✅ 推荐:使用data-testid
|
||||
await page.getByTestId('submit-button').click();
|
||||
```
|
||||
|
||||
### 3. 等待策略
|
||||
```typescript
|
||||
// ❌ 不推荐:固定等待
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// ✅ 推荐:等待特定条件
|
||||
await expect(page.locator('.success-message')).toBeVisible();
|
||||
```
|
||||
|
||||
### 4. 测试独立性
|
||||
- 每个测试应该独立运行
|
||||
- 不要依赖其他测试的执行顺序
|
||||
- 使用beforeEach/afterEach进行设置和清理
|
||||
|
||||
### 5. 使用test.step提高可读性
|
||||
```typescript
|
||||
await test.step('1. 登录系统', async () => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
});
|
||||
|
||||
await test.step('2. 创建用户', async () => {
|
||||
await userPage.clickCreateUser();
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 使用调试模式
|
||||
```bash
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
### 2. 使用有头模式
|
||||
```bash
|
||||
npx playwright test --headed
|
||||
```
|
||||
|
||||
### 3. 查看trace文件
|
||||
```bash
|
||||
npx playwright show-trace trace.zip
|
||||
```
|
||||
|
||||
### 4. 截图和视频
|
||||
Playwright会在测试失败时自动截图和录制视频,存储在:
|
||||
- `test-results/` 目录
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 问题1:浏览器启动失败
|
||||
```bash
|
||||
npx playwright install --with-deps chromium
|
||||
```
|
||||
|
||||
### 问题2:连接超时
|
||||
检查后端服务是否正常运行:
|
||||
```bash
|
||||
curl http://localhost:8084/actuator/health
|
||||
```
|
||||
|
||||
### 问题3:元素定位失败
|
||||
使用Playwright Inspector检查元素:
|
||||
```bash
|
||||
npx playwright codegen http://localhost:3003
|
||||
```
|
||||
|
||||
## 测试报告
|
||||
|
||||
测试执行后会生成以下报告:
|
||||
|
||||
1. **HTML报告**: `playwright-report/index.html`
|
||||
2. **JUnit报告**: `test-results/junit.xml`
|
||||
3. **Trace文件**: `test-results/trace.zip` (失败时)
|
||||
|
||||
## 贡献指南
|
||||
|
||||
添加新的E2E测试:
|
||||
|
||||
1. 在`pages/`目录创建对应的Page类
|
||||
2. 在`e2e/`目录创建测试文件
|
||||
3. 使用Page Object Model编写测试
|
||||
4. 确保测试独立性和可重复性
|
||||
5. 添加适当的断言和验证
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Playwright官方文档](https://playwright.dev/)
|
||||
- [Page Object Model最佳实践](https://playwright.dev/docs/pom)
|
||||
- [测试最佳实践](https://playwright.dev/docs/best-practices)
|
||||
@@ -1,361 +0,0 @@
|
||||
# E2E测试覆盖分析报告
|
||||
|
||||
## 📊 测试文件统计
|
||||
|
||||
### 测试文件列表
|
||||
|
||||
| 序号 | 测试文件 | 测试类型 | 状态 | 测试数量 |
|
||||
|------|---------|---------|------|---------|
|
||||
| 1 | basic.spec.ts | 基础功能 | ⚠️ 部分失败 | 6 |
|
||||
| 2 | auth.spec.ts | 认证功能 | ❌ 未测试 | 待定 |
|
||||
| 3 | user-management.spec.ts | 用户管理 | ❌ 未测试 | 待定 |
|
||||
| 4 | role-management.spec.ts | 角色管理 | ❌ 未测试 | 待定 |
|
||||
| 5 | system-config.spec.ts | 系统配置 | ❌ 未测试 | 待定 |
|
||||
| 6 | complete-workflow.spec.ts | 完整流程 | ❌ 未测试 | 待定 |
|
||||
| 7 | uat-phase1.spec.ts | UAT阶段一 | ❌ 全部失败 | 7 |
|
||||
| 8 | simple-api.spec.ts | API测试 | ✅ 全部通过 | 2 |
|
||||
| 9 | diagnostic.spec.ts | 诊断测试 | ✅ 部分通过 | 4 |
|
||||
| 10 | headless-test.spec.ts | Headless测试 | ❌ 全部失败 | 3 |
|
||||
|
||||
**总计**:10个测试文件,约35个测试场景
|
||||
|
||||
### 测试通过率统计
|
||||
|
||||
| 测试类型 | 总数 | 通过 | 失败 | 通过率 |
|
||||
|---------|------|------|------|--------|
|
||||
| API测试 | 2 | 2 | 0 | 100% |
|
||||
| 基础功能 | 6 | 0 | 6 | 0% |
|
||||
| UAT测试 | 7 | 0 | 7 | 0% |
|
||||
| 诊断测试 | 4 | 1 | 3 | 25% |
|
||||
| **总计** | **19** | **3** | **16** | **15.8%** |
|
||||
|
||||
## 🎯 功能模块覆盖分析
|
||||
|
||||
### 已覆盖的功能模块
|
||||
|
||||
#### ✅ 后端API功能(100%覆盖)
|
||||
- [x] 健康检查API
|
||||
- [x] 登录认证API
|
||||
- [x] 数据库连接验证
|
||||
- [x] 后端服务状态检查
|
||||
|
||||
**测试质量**:⭐⭐⭐⭐⭐ (优秀)
|
||||
- 所有API测试100%通过
|
||||
- 响应时间<300ms
|
||||
- 错误处理完善
|
||||
|
||||
#### ⚠️ 基础功能(0%覆盖)
|
||||
- [ ] 首页加载测试
|
||||
- [ ] 登录页面访问测试
|
||||
- [ ] 后端健康检查(页面)
|
||||
- [ ] 数据库连接检查(页面)
|
||||
- [ ] 前端页面可访问性
|
||||
- [ ] API代理配置验证
|
||||
|
||||
**测试质量**:⭐☆☆☆☆ (差)
|
||||
- 所有页面测试失败
|
||||
- 前端服务不稳定
|
||||
- 需要修复环境问题
|
||||
|
||||
#### ❌ 业务功能(0%覆盖)
|
||||
- [ ] 用户管理功能
|
||||
- [ ] 角色管理功能
|
||||
- [ ] 系统配置功能
|
||||
- [ ] 完整业务流程
|
||||
|
||||
**测试质量**:⭐☆☆☆☆ (无)
|
||||
- 未执行业务功能测试
|
||||
- 缺少核心业务场景覆盖
|
||||
- 需要补充测试用例
|
||||
|
||||
#### ❌ UAT场景(0%覆盖)
|
||||
- [ ] 用户认证流程
|
||||
- [ ] 系统管理导航
|
||||
- [ ] 用户管理操作
|
||||
- [ ] 角色管理操作
|
||||
- [ ] 系统配置操作
|
||||
- [ ] 完整业务流程
|
||||
|
||||
**测试质量**:⭐☆☆☆☆ (无)
|
||||
- 所有UAT测试失败
|
||||
- 核心用户场景未验证
|
||||
- 无法进行用户验收测试
|
||||
|
||||
## 📋 测试场景详细分析
|
||||
|
||||
### Phase 1: 基础设施测试
|
||||
|
||||
#### 测试目标
|
||||
验证系统基础设施的可用性和稳定性
|
||||
|
||||
#### 测试场景
|
||||
1. ✅ 后端健康检查(API)- 通过
|
||||
2. ✅ 登录API测试 - 通过
|
||||
3. ❌ 首页加载测试 - 失败
|
||||
4. ❌ 登录页面访问 - 失败
|
||||
5. ❌ 前端页面可访问性 - 失败
|
||||
|
||||
#### 覆盖率:40% (2/5)
|
||||
#### 状态:部分完成
|
||||
|
||||
### Phase 2: 认证功能测试
|
||||
|
||||
#### 测试目标
|
||||
验证用户认证和授权功能的正确性
|
||||
|
||||
#### 测试场景
|
||||
1. ❌ 成功登录流程 - 未测试
|
||||
2. ❌ 登录失败处理 - 未测试
|
||||
3. ❌ 登出功能 - 未测试
|
||||
4. ❌ 会话管理 - 未测试
|
||||
|
||||
#### 覆盖率:0% (0/4)
|
||||
#### 状态:未开始
|
||||
|
||||
### Phase 3: 业务功能测试
|
||||
|
||||
#### 测试目标
|
||||
验证核心业务功能的正确性和完整性
|
||||
|
||||
#### 测试场景
|
||||
1. ❌ 用户管理CRUD - 未测试
|
||||
2. ❌ 角色管理CRUD - 未测试
|
||||
3. ❌ 系统配置管理 - 未测试
|
||||
4. ❌ 权限验证 - 未测试
|
||||
|
||||
#### 覆盖率:0% (0/4)
|
||||
#### 状态:未开始
|
||||
|
||||
### Phase 4: 完整流程测试
|
||||
|
||||
#### 测试目标
|
||||
验证端到端业务流程的完整性
|
||||
|
||||
#### 测试场景
|
||||
1. ❌ 用户注册到登录流程 - 未测试
|
||||
2. ❌ 完整业务操作流程 - 未测试
|
||||
3. ❌ 跨模块集成测试 - 未测试
|
||||
|
||||
#### 覆盖率:0% (0/3)
|
||||
#### 状态:未开始
|
||||
|
||||
## 🚨 测试覆盖差距分析
|
||||
|
||||
### 关键缺失的测试场景
|
||||
|
||||
#### 高优先级缺失(P0)
|
||||
|
||||
1. **用户认证完整流程**
|
||||
- 缺失:登录、登出、会话管理
|
||||
- 影响:无法验证核心安全功能
|
||||
- 优先级:P0(最高)
|
||||
|
||||
2. **用户管理核心功能**
|
||||
- 缺失:用户CRUD、搜索、分页
|
||||
- 影响:无法验证用户管理功能
|
||||
- 优先级:P0(最高)
|
||||
|
||||
3. **角色权限管理**
|
||||
- 缺失:角色分配、权限验证
|
||||
- 影响:无法验证权限控制
|
||||
- 优先级:P0(最高)
|
||||
|
||||
#### 中优先级缺失(P1)
|
||||
|
||||
1. **系统配置管理**
|
||||
- 缺失:参数配置、字典管理
|
||||
- 影响:无法验证系统配置功能
|
||||
- 优先级:P1(高)
|
||||
|
||||
2. **业务流程集成**
|
||||
- 缺失:跨模块业务流程
|
||||
- 影响:无法验证系统集成
|
||||
- 优先级:P1(高)
|
||||
|
||||
#### 低优先级缺失(P2)
|
||||
|
||||
1. **性能测试**
|
||||
- 缺失:页面加载性能、API响应时间
|
||||
- 影响:无法评估系统性能
|
||||
- 优先级:P2(中)
|
||||
|
||||
2. **安全测试**
|
||||
- 缺失:XSS、CSRF、SQL注入
|
||||
- 影响:无法验证安全性
|
||||
- 优先级:P2(中)
|
||||
|
||||
## 📊 测试质量评估
|
||||
|
||||
### 测试代码质量
|
||||
|
||||
#### 优势
|
||||
- ✅ 使用Page Object Model模式
|
||||
- ✅ 测试结构清晰,易于维护
|
||||
- ✅ 测试数据管理完善
|
||||
- ✅ API测试质量高
|
||||
|
||||
#### 劣势
|
||||
- ❌ 测试稳定性差(通过率15.8%)
|
||||
- ❌ 环境依赖性强
|
||||
- ❌ 缺少测试重试机制
|
||||
- ❌ 错误处理不完善
|
||||
|
||||
### 测试执行效率
|
||||
|
||||
#### 当前状况
|
||||
- 平均测试执行时间:30-40秒/测试
|
||||
- 测试失败率:84.2%
|
||||
- 调试时间占比:高
|
||||
|
||||
#### 改进建议
|
||||
1. 优化测试等待策略
|
||||
2. 增加测试重试机制
|
||||
3. 改进错误处理和日志
|
||||
4. 建立测试并行执行
|
||||
|
||||
## 🎯 测试覆盖提升计划
|
||||
|
||||
### 短期目标(1周内)
|
||||
|
||||
#### 目标:提升测试通过率到50%
|
||||
|
||||
**行动计划**:
|
||||
1. 修复前端服务环境问题
|
||||
- 使用Docker容器化环境
|
||||
- 建立稳定的测试环境
|
||||
- 预期效果:测试通过率提升至50%
|
||||
|
||||
2. 修复现有测试失败问题
|
||||
- 分析失败原因
|
||||
- 修复定位器和等待策略
|
||||
- 预期效果:现有测试通过率提升至80%
|
||||
|
||||
3. 补充关键测试场景
|
||||
- 用户认证流程测试
|
||||
- 用户管理基础测试
|
||||
- 预期效果:测试覆盖提升至30%
|
||||
|
||||
### 中期目标(2周内)
|
||||
|
||||
#### 目标:提升测试覆盖到70%
|
||||
|
||||
**行动计划**:
|
||||
1. 完善业务功能测试
|
||||
- 用户管理完整测试
|
||||
- 角色管理完整测试
|
||||
- 系统配置管理测试
|
||||
- 预期效果:业务功能覆盖达到60%
|
||||
|
||||
2. 实现完整流程测试
|
||||
- 端到端业务流程
|
||||
- 跨模块集成测试
|
||||
- 预期效果:流程覆盖达到50%
|
||||
|
||||
3. 优化测试稳定性
|
||||
- 增加重试机制
|
||||
- 改进等待策略
|
||||
- 预期效果:测试通过率达到80%
|
||||
|
||||
### 长期目标(1月内)
|
||||
|
||||
#### 目标:达到企业级测试覆盖
|
||||
|
||||
**行动计划**:
|
||||
1. 建立全面测试体系
|
||||
- 单元测试、集成测试、E2E测试
|
||||
- 性能测试、安全测试
|
||||
- 预期效果:测试覆盖达到90%
|
||||
|
||||
2. 实现持续测试机制
|
||||
- CI/CD集成
|
||||
- 自动化测试执行
|
||||
- 预期效果:测试自动化程度达到95%
|
||||
|
||||
3. 建立测试质量门禁
|
||||
- 代码覆盖率要求
|
||||
- 测试通过率要求
|
||||
- 预期效果:测试质量标准化
|
||||
|
||||
## 📋 测试框架改进建议
|
||||
|
||||
### 立即改进(1-2天)
|
||||
|
||||
1. **环境稳定性**
|
||||
- 使用Docker容器化
|
||||
- 建立环境健康检查
|
||||
- 实现环境自动恢复
|
||||
|
||||
2. **测试配置优化**
|
||||
- 增加测试超时配置
|
||||
- 配置测试重试策略
|
||||
- 优化并行执行参数
|
||||
|
||||
3. **测试数据管理**
|
||||
- 建立测试数据工厂
|
||||
- 实现数据清理机制
|
||||
- 支持测试数据版本控制
|
||||
|
||||
### 短期改进(3-7天)
|
||||
|
||||
1. **测试框架增强**
|
||||
- 实现测试基类
|
||||
- 建立测试工具库
|
||||
- 完善断言库
|
||||
|
||||
2. **测试报告优化**
|
||||
- 生成详细测试报告
|
||||
- 实现测试趋势分析
|
||||
- 建立缺陷跟踪机制
|
||||
|
||||
3. **测试文档完善**
|
||||
- 编写测试最佳实践
|
||||
- 建立测试维护指南
|
||||
- 创建测试培训材料
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
### 当前状态
|
||||
|
||||
**测试框架成熟度**:⭐⭐⭐☆☆ (3/5)
|
||||
- 基础设施:⭐⭐⭐⭐⭐ (4/5)
|
||||
- 测试覆盖:⭐⭐☆☆☆ (2/5)
|
||||
- 测试质量:⭐⭐⭐☆☆ (3/5)
|
||||
- 执行效率:⭐☆☆☆☆ (1/5)
|
||||
|
||||
### 核心优势
|
||||
|
||||
1. ✅ 后端API测试完全就绪
|
||||
2. ✅ 测试基础设施完善
|
||||
3. ✅ Page Object Model实现
|
||||
4. ✅ 测试数据管理健全
|
||||
|
||||
### 主要挑战
|
||||
|
||||
1. ❌ 前端测试环境不稳定
|
||||
2. ❌ 测试通过率低(15.8%)
|
||||
3. ❌ 业务功能覆盖不足
|
||||
4. ❌ 测试执行效率低
|
||||
|
||||
### 改进路径
|
||||
|
||||
**短期**(1周内):
|
||||
- 修复环境问题
|
||||
- 提升测试通过率到50%
|
||||
- 补充关键测试场景
|
||||
|
||||
**中期**(2周内):
|
||||
- 完善业务功能测试
|
||||
- 实现完整流程测试
|
||||
- 提升测试覆盖到70%
|
||||
|
||||
**长期**(1月内):
|
||||
- 建立全面测试体系
|
||||
- 实现持续测试机制
|
||||
- 达到企业级测试标准
|
||||
|
||||
---
|
||||
|
||||
**报告版本**:v1.0
|
||||
**生成时间**:2026-03-17
|
||||
**分析人员**:张翔
|
||||
**下次更新**:测试改进后重新评估
|
||||
@@ -1,617 +0,0 @@
|
||||
# UAT测试框架准备度评估报告
|
||||
|
||||
## 📊 执行摘要
|
||||
|
||||
**评估日期**:2026-03-17
|
||||
**评估人员**:张翔
|
||||
**评估方法**:系统化调试
|
||||
**评估结论**:⚠️ **部分就绪** - 后端测试框架健全,前端服务存在关键问题
|
||||
|
||||
---
|
||||
|
||||
## 🔍 系统化调试过程
|
||||
|
||||
### Phase 1: 根本原因调查
|
||||
|
||||
#### 1.1 仔细阅读错误信息
|
||||
**主要错误模式**:
|
||||
```
|
||||
Error: page.goto: net::ERR_ABORTED; maybe frame was detached?
|
||||
Call log:
|
||||
- navigating to "http://localhost:3001/login", waiting until "load"
|
||||
```
|
||||
|
||||
**错误特征**:
|
||||
- 所有前端页面访问测试都失败
|
||||
- 错误一致:`net::ERR_ABORTED`
|
||||
- 测试超时:30秒后失败
|
||||
- 影响范围:所有使用`page.goto()`的测试
|
||||
|
||||
#### 1.2 一致性重现问题
|
||||
**诊断测试结果**:
|
||||
- ✅ 后端健康检查:通过(200 OK)
|
||||
- ✅ 登录API:通过(返回有效token)
|
||||
- ❌ 前端页面访问:全部失败
|
||||
- ❌ curl访问localhost:3001:超时失败
|
||||
|
||||
**关键发现**:问题不是Playwright特定,而是前端服务本身无法响应HTTP请求。
|
||||
|
||||
#### 1.3 检查最近的变更
|
||||
**Playwright配置**:
|
||||
```typescript
|
||||
use: {
|
||||
baseURL: 'http://localhost:3001',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
headless: true, // 原始配置
|
||||
}
|
||||
```
|
||||
|
||||
**前端服务配置**:
|
||||
```typescript
|
||||
server: {
|
||||
port: 3001,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8084',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 在多组件系统中收集证据
|
||||
|
||||
**组件边界测试结果**:
|
||||
|
||||
| 组件 | 测试方法 | 结果 | 状态 |
|
||||
|--------|---------|------|------|
|
||||
| 后端服务 | API请求 | ✅ 通过 | 正常 |
|
||||
| 数据库 | 健康检查 | ✅ 通过 | 正常 |
|
||||
| 前端服务 | HTTP请求 | ❌ 失败 | 异常 |
|
||||
| 浏览器自动化 | Playwright | ❌ 失败 | 受影响 |
|
||||
|
||||
#### 1.5 追踪数据流
|
||||
|
||||
**数据流分析**:
|
||||
```
|
||||
Playwright → HTTP请求 → localhost:3001 → Vite服务 → 响应
|
||||
↓ ↓ ↓ ↓
|
||||
正常 超时 挂起状态 无响应
|
||||
```
|
||||
|
||||
**根本问题**:Vite进程虽然显示"ready",但实际处于挂起状态(TN状态)。
|
||||
|
||||
### Phase 2: 模式分析
|
||||
|
||||
#### 2.1 寻找工作示例
|
||||
|
||||
**成功的工作示例**:
|
||||
```typescript
|
||||
// simple-api.spec.ts - API测试完全正常
|
||||
test('后端健康检查', async ({ request }) => {
|
||||
const response = await request.get('http://localhost:8084/actuator/health');
|
||||
expect(response.status()).toBe(200);
|
||||
// ✅ 通过 - 86ms
|
||||
});
|
||||
|
||||
test('登录API', async ({ request }) => {
|
||||
const response = await request.post('http://localhost:8084/api/auth/login', {
|
||||
data: { username: 'admin', password: 'password' }
|
||||
});
|
||||
expect(response.status()).toBe(200);
|
||||
// ✅ 通过 - 295ms
|
||||
});
|
||||
```
|
||||
|
||||
**失败的工作示例**:
|
||||
```typescript
|
||||
// 所有使用page.goto的测试都失败
|
||||
test('前端页面访问', async ({ page }) => {
|
||||
await page.goto('http://localhost:3001/login');
|
||||
// ❌ 失败 - Timeout 30000ms exceeded
|
||||
});
|
||||
```
|
||||
|
||||
#### 2.2 对比工作示例
|
||||
|
||||
**成功模式**:
|
||||
- 使用`request`对象进行API调用
|
||||
- 直接访问后端服务
|
||||
- 不依赖前端页面渲染
|
||||
|
||||
**失败模式**:
|
||||
- 使用`page.goto()`访问前端页面
|
||||
- 依赖Vite服务响应
|
||||
- 需要页面加载和渲染
|
||||
|
||||
#### 2.3 识别差异
|
||||
|
||||
| 特征 | API测试 | 页面测试 |
|
||||
|------|---------|---------|
|
||||
| 测试对象 | 后端服务 | 前端服务 |
|
||||
| 通信方式 | HTTP请求 | 浏览器渲染 |
|
||||
| 成功率 | 100% (2/2) | 0% (0/7) |
|
||||
| 响应时间 | <300ms | 超时 |
|
||||
|
||||
#### 2.4 理解依赖关系
|
||||
|
||||
**测试依赖图**:
|
||||
```
|
||||
UAT测试
|
||||
├── API测试 (✅ 可用)
|
||||
│ ├── 后端服务
|
||||
│ ├── 数据库
|
||||
│ └── 认证系统
|
||||
└── 页面测试 (❌ 不可用)
|
||||
├── 前端Vite服务
|
||||
├── 页面路由
|
||||
└── 浏览器自动化
|
||||
```
|
||||
|
||||
### Phase 3: 假设和测试
|
||||
|
||||
#### 3.1 形成单一假设
|
||||
|
||||
**假设1**:Playwright的headless模式与Vite服务存在兼容性问题
|
||||
- **测试结果**:❌ 失败 - 改为headless=false后仍然失败
|
||||
- **结论**:假设不成立
|
||||
|
||||
**假设2**:前端Vite服务启动失败或运行异常
|
||||
- **测试结果**:✅ 确认 - curl也无法访问,进程状态异常
|
||||
- **结论**:假设成立
|
||||
|
||||
**假设3**:端口冲突导致服务无法正常响应
|
||||
- **测试结果**:❌ 排除 - lsof显示端口被Vite进程占用
|
||||
- **结论**:假设不成立
|
||||
|
||||
#### 3.2 最小化测试验证
|
||||
|
||||
**验证测试**:
|
||||
```bash
|
||||
# 测试1: 直接curl访问
|
||||
curl -m 5 http://localhost:3001
|
||||
# 结果:curl: (28) Operation timed out
|
||||
|
||||
# 测试2: 检查进程状态
|
||||
ps -p 97632 -o pid,stat,command
|
||||
# 结果:97632 TN node ... (TN = stopped, waiting for job control)
|
||||
|
||||
# 测试3: 检查端口监听
|
||||
lsof -i:3001
|
||||
# 结果:node进程在监听,但无法响应
|
||||
```
|
||||
|
||||
#### 3.3 验证修复前
|
||||
|
||||
**根本原因确认**:
|
||||
- Vite进程状态为`TN`(stopped and waiting for job control signal)
|
||||
- 进程虽然在监听端口3001,但无法处理HTTP请求
|
||||
- 这解释了为什么所有前端页面访问都超时
|
||||
|
||||
### Phase 4: 实施建议
|
||||
|
||||
#### 4.1 创建失败的测试用例
|
||||
|
||||
**已创建的诊断测试**:
|
||||
- `diagnostic.spec.ts` - 环境诊断测试
|
||||
- `simple-api.spec.ts` - API测试(成功)
|
||||
- `headless-test.spec.ts` - Headless模式测试
|
||||
|
||||
#### 4.2 根本原因修复方案
|
||||
|
||||
**方案1:修复Vite服务启动问题**
|
||||
```bash
|
||||
# 停止所有挂起的进程
|
||||
lsof -ti:3001 | xargs kill -9
|
||||
|
||||
# 重新启动前端服务
|
||||
cd /Users/zhangxiang/Codes/Novalon/novalon-manage-system/novalon-manage-web
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**方案2:使用不同的启动方式**
|
||||
```bash
|
||||
# 使用nohup避免进程挂起
|
||||
nohup npm run dev > /tmp/frontend.log 2>&1 &
|
||||
|
||||
# 或使用screen/tmux
|
||||
screen -S frontend
|
||||
npm run dev
|
||||
# Ctrl+A, D 分离会话
|
||||
```
|
||||
|
||||
**方案3:使用生产构建进行测试**
|
||||
```bash
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 使用预览服务器
|
||||
npm run preview
|
||||
```
|
||||
|
||||
#### 4.3 验证修复
|
||||
|
||||
**验证步骤**:
|
||||
1. 启动前端服务
|
||||
2. 使用curl验证服务可访问
|
||||
3. 运行简单的页面测试
|
||||
4. 逐步扩大测试范围
|
||||
|
||||
---
|
||||
|
||||
## 📊 UAT准备度评估
|
||||
|
||||
### 测试框架成熟度评估
|
||||
|
||||
#### 后端测试框架:⭐⭐⭐⭐⭐ (5/5)
|
||||
|
||||
**优势**:
|
||||
- ✅ 单元测试覆盖全面:494个测试
|
||||
- ✅ API测试完全正常:健康检查、登录API都通过
|
||||
- ✅ 测试基础设施健全:测试报告、覆盖率报告完善
|
||||
- ✅ CI/CD集成:Woodpecker CI配置完成
|
||||
- ✅ 测试稳定性高:所有API测试100%通过
|
||||
|
||||
**准备度**:**完全就绪** - 可以进行后端UAT测试
|
||||
|
||||
#### 前端测试框架:⭐⭐☆☆☆ (2/5)
|
||||
|
||||
**优势**:
|
||||
- ✅ Playwright配置完善
|
||||
- ✅ Page Object Model实现完整
|
||||
- ✅ 测试场景设计合理
|
||||
- ✅ 测试数据管理健全
|
||||
|
||||
**劣势**:
|
||||
- ❌ 前端服务启动不稳定
|
||||
- ❌ 页面访问测试全部失败
|
||||
- ❌ 环境配置存在问题
|
||||
- ❌ 测试执行成功率0%
|
||||
|
||||
**准备度**:**部分就绪** - 需要修复前端服务问题
|
||||
|
||||
### UAT测试能力评估
|
||||
|
||||
#### 已具备的测试能力
|
||||
|
||||
| 测试类型 | 能力 | 状态 | 备注 |
|
||||
|---------|------|------|------|
|
||||
| 后端API测试 | ✅ 完全具备 | 可立即执行 |
|
||||
| 数据库集成测试 | ✅ 完全具备 | 可立即执行 |
|
||||
| 认证流程测试 | ✅ 完全具备 | API层面可用 |
|
||||
| 前端页面测试 | ❌ 不具备 | 需要修复服务 |
|
||||
| 端到端流程测试 | ❌ 不具备 | 需要修复服务 |
|
||||
| 用户界面测试 | ❌ 不具备 | 需要修复服务 |
|
||||
|
||||
#### UAT场景覆盖分析
|
||||
|
||||
**UAT测试计划覆盖**:
|
||||
|
||||
| UAT场景 | 测试类型 | 可执行性 | 状态 |
|
||||
|---------|---------|----------|------|
|
||||
| 用户认证流程 | 前端页面 | ❌ 不可执行 | 阻塞 |
|
||||
| 系统管理导航 | 前端页面 | ❌ 不可执行 | 阻塞 |
|
||||
| 用户管理功能 | 前端页面 | ❌ 不可执行 | 阻塞 |
|
||||
| 角色管理功能 | 前端页面 | ❌ 不可执行 | 阻塞 |
|
||||
| API接口测试 | 后端API | ✅ 可执行 | 可用 |
|
||||
| 数据库操作 | 后端API | ✅ 可执行 | 可用 |
|
||||
|
||||
**当前可执行UAT**:**20%** (1/5场景)
|
||||
**目标UAT覆盖率**:**100%** (5/5场景)
|
||||
|
||||
### 测试基础设施评估
|
||||
|
||||
#### 测试环境
|
||||
|
||||
| 组件 | 状态 | 稳定性 | 备注 |
|
||||
|------|------|---------|------|
|
||||
| 后端服务 | ✅ 正常 | 高 | 稳定运行 |
|
||||
| 数据库服务 | ✅ 正常 | 高 | 连接正常 |
|
||||
| 前端服务 | ❌ 异常 | 低 | 进程挂起 |
|
||||
| 测试浏览器 | ✅ 正常 | 高 | Playwright正常 |
|
||||
|
||||
#### 测试工具链
|
||||
|
||||
| 工具 | 配置 | 状态 | 备注 |
|
||||
|------|------|------|------|
|
||||
| Playwright | ✅ 完整配置 | 正常 | 配置完善 |
|
||||
| Page Object Model | ✅ 已实现 | 正常 | 结构清晰 |
|
||||
| 测试报告 | ✅ 已配置 | 正常 | HTML/JUnit |
|
||||
| CI/CD集成 | ✅ 已配置 | 正常 | Woodpecker |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 UAT准备度结论
|
||||
|
||||
### 总体评估
|
||||
|
||||
**UAT准备度**:⚠️ **部分就绪** (60/100)
|
||||
|
||||
**评分明细**:
|
||||
- 后端测试框架:25/25 (100%)
|
||||
- 前端测试框架:10/25 (40%)
|
||||
- 测试基础设施:15/25 (60%)
|
||||
- UAT场景覆盖:10/25 (40%)
|
||||
|
||||
### 可以进行的UAT测试
|
||||
|
||||
#### ✅ 立即可执行
|
||||
|
||||
1. **后端API UAT**
|
||||
- 认证API测试
|
||||
- 用户管理API测试
|
||||
- 角色管理API测试
|
||||
- 系统配置API测试
|
||||
|
||||
2. **数据库集成测试**
|
||||
- 数据持久化测试
|
||||
- 事务处理测试
|
||||
- 数据一致性测试
|
||||
|
||||
#### ❌ 需要修复后执行
|
||||
|
||||
1. **前端页面UAT**
|
||||
- 用户登录界面测试
|
||||
- 系统导航测试
|
||||
- 页面交互测试
|
||||
|
||||
2. **端到端流程测试**
|
||||
- 完整业务流程测试
|
||||
- 跨模块集成测试
|
||||
- 用户体验测试
|
||||
|
||||
### 阻塞问题
|
||||
|
||||
#### 关键阻塞
|
||||
|
||||
**问题1:前端Vite服务无法正常响应**
|
||||
- **严重程度**:🔴 严重
|
||||
- **影响范围**:所有前端页面测试
|
||||
- **修复优先级**:P0(最高)
|
||||
- **预计修复时间**:1-2小时
|
||||
|
||||
**问题2:测试环境不稳定**
|
||||
- **严重程度**:🟡 中等
|
||||
- **影响范围**:测试执行可靠性
|
||||
- **修复优先级**:P1(高)
|
||||
- **预计修复时间**:2-4小时
|
||||
|
||||
### 风险评估
|
||||
|
||||
#### 高风险项
|
||||
|
||||
1. **前端服务稳定性风险**
|
||||
- **风险描述**:Vite服务启动后经常挂起
|
||||
- **影响范围**:所有前端UAT测试
|
||||
- **缓解措施**:使用生产构建进行测试
|
||||
- **备选方案**:使用Docker容器化环境
|
||||
|
||||
2. **测试环境配置风险**
|
||||
- **风险描述**:本地开发环境配置复杂
|
||||
- **影响范围**:测试可重复性
|
||||
- **缓解措施**:建立标准化测试环境
|
||||
- **备选方案**:使用CI/CD环境进行UAT
|
||||
|
||||
#### 中风险项
|
||||
|
||||
1. **测试覆盖率不足风险**
|
||||
- **风险描述**:当前只能测试后端API
|
||||
- **影响范围**:UAT完整性
|
||||
- **缓解措施**:优先修复前端服务
|
||||
- **备选方案**:手动补充前端测试
|
||||
|
||||
2. **测试执行效率风险**
|
||||
- **风险描述**:测试失败率高,调试时间长
|
||||
- **影响范围**:UAT进度
|
||||
- **缓解措施**:优化测试配置
|
||||
- **备选方案**:增加测试重试机制
|
||||
|
||||
---
|
||||
|
||||
## 📋 行动建议
|
||||
|
||||
### 立即行动(1-2天)
|
||||
|
||||
#### 优先级P0:修复前端服务问题
|
||||
|
||||
**目标**:使前端Vite服务能够正常响应HTTP请求
|
||||
|
||||
**行动步骤**:
|
||||
1. 停止所有挂起的Vite进程
|
||||
```bash
|
||||
lsof -ti:3001 | xargs kill -9
|
||||
```
|
||||
|
||||
2. 使用nohup重新启动前端服务
|
||||
```bash
|
||||
cd /Users/zhangxiang/Codes/Novalon/novalon-manage-system/novalon-manage-web
|
||||
nohup npm run dev > /tmp/frontend.log 2>&1 &
|
||||
```
|
||||
|
||||
3. 验证服务可访问性
|
||||
```bash
|
||||
curl -I http://localhost:3001
|
||||
```
|
||||
|
||||
4. 运行简单的页面测试验证
|
||||
```bash
|
||||
npx playwright test basic.spec.ts -g "首页加载测试"
|
||||
```
|
||||
|
||||
**成功标准**:
|
||||
- curl能够成功访问localhost:3001
|
||||
- 简单的页面测试能够通过
|
||||
- 前端服务进程状态正常(S或R状态)
|
||||
|
||||
#### 优先级P1:执行后端UAT测试
|
||||
|
||||
**目标**:在修复前端服务的同时,先进行后端UAT
|
||||
|
||||
**行动步骤**:
|
||||
1. 执行所有API测试
|
||||
```bash
|
||||
npx playwright test simple-api.spec.ts
|
||||
```
|
||||
|
||||
2. 验证后端功能完整性
|
||||
- 用户认证API
|
||||
- 数据CRUD操作
|
||||
- 权限验证
|
||||
|
||||
3. 生成后端UAT报告
|
||||
- API响应时间
|
||||
- 功能覆盖率
|
||||
- 缺陷统计
|
||||
|
||||
### 短期行动(3-7天)
|
||||
|
||||
#### 优先级P2:建立稳定测试环境
|
||||
|
||||
**目标**:建立可重复、稳定的UAT测试环境
|
||||
|
||||
**行动步骤**:
|
||||
1. 使用Docker容器化测试环境
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
frontend:
|
||||
build: ./novalon-manage-web
|
||||
ports:
|
||||
- "3001:3001"
|
||||
backend:
|
||||
build: ./novalon-manage-api
|
||||
ports:
|
||||
- "8084:8084"
|
||||
database:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_DB: manage_system
|
||||
```
|
||||
|
||||
2. 配置环境变量和依赖
|
||||
3. 建立环境健康检查脚本
|
||||
4. 编写环境启动文档
|
||||
|
||||
#### 优先级P3:完善测试覆盖
|
||||
|
||||
**目标**:达到100%的UAT场景覆盖
|
||||
|
||||
**行动步骤**:
|
||||
1. 修复所有失败的E2E测试
|
||||
2. 添加缺失的测试场景
|
||||
3. 优化测试稳定性和性能
|
||||
4. 建立测试报告自动化
|
||||
|
||||
### 中期行动(1-2周)
|
||||
|
||||
#### 优先级P4:建立持续UAT机制
|
||||
|
||||
**目标**:实现定期、自动化的UAT测试
|
||||
|
||||
**行动步骤**:
|
||||
1. 配置CI/CD流水线
|
||||
- 每次PR自动运行UAT
|
||||
- 每日定时运行完整UAT
|
||||
- 生成UAT趋势报告
|
||||
|
||||
2. 建立UAT测试门户
|
||||
- 实时查看UAT结果
|
||||
- 历史趋势分析
|
||||
- 缺陷跟踪和管理
|
||||
|
||||
3. 建立UAT质量门禁
|
||||
- UAT通过率≥70%才能合并
|
||||
- 严重缺陷必须修复
|
||||
- 新功能必须有UAT覆盖
|
||||
|
||||
---
|
||||
|
||||
## 📊 测试框架优势
|
||||
|
||||
### 已建立的优势
|
||||
|
||||
#### 1. 完善的测试基础设施
|
||||
- ✅ Playwright配置完整
|
||||
- ✅ Page Object Model实现
|
||||
- ✅ 测试数据管理健全
|
||||
- ✅ 测试报告自动化
|
||||
|
||||
#### 2. 全面的后端测试覆盖
|
||||
- ✅ 494个单元测试
|
||||
- ✅ API测试完全正常
|
||||
- ✅ 数据库集成测试完善
|
||||
- ✅ 测试稳定性高
|
||||
|
||||
#### 3. 标准化的测试流程
|
||||
- ✅ UAT测试计划完整
|
||||
- ✅ 测试场景定义清晰
|
||||
- ✅ 测试报告模板完善
|
||||
- ✅ CI/CD集成完成
|
||||
|
||||
#### 4. 专业的测试实践
|
||||
- ✅ 系统化调试方法
|
||||
- ✅ 根本原因分析
|
||||
- ✅ 测试驱动开发
|
||||
- ✅ 持续集成测试
|
||||
|
||||
---
|
||||
|
||||
## 🎯 最终结论
|
||||
|
||||
### UAT准备度总结
|
||||
|
||||
**总体评估**:⚠️ **部分就绪** (60/100)
|
||||
|
||||
**可以立即进行的UAT**:
|
||||
- ✅ 后端API测试(100%可用)
|
||||
- ✅ 数据库集成测试(100%可用)
|
||||
- ✅ 认证流程测试(API层面)
|
||||
|
||||
**需要修复后进行的UAT**:
|
||||
- ❌ 前端页面测试(0%可用)
|
||||
- ❌ 端到端流程测试(0%可用)
|
||||
- ❌ 用户界面测试(0%可用)
|
||||
|
||||
### 核心建议
|
||||
|
||||
1. **立即修复前端服务问题**(1-2小时)
|
||||
- 这是当前唯一的阻塞问题
|
||||
- 修复后可以进行完整的UAT
|
||||
|
||||
2. **并行进行后端UAT**(立即开始)
|
||||
- 不要等待前端修复
|
||||
- 先验证后端功能完整性
|
||||
|
||||
3. **建立稳定测试环境**(3-7天)
|
||||
- 使用Docker容器化
|
||||
- 提高测试可重复性
|
||||
|
||||
4. **完善测试覆盖**(1-2周)
|
||||
- 达到100% UAT场景覆盖
|
||||
- 建立持续UAT机制
|
||||
|
||||
### 成功标准
|
||||
|
||||
**短期目标**(1周内):
|
||||
- 前端服务问题修复
|
||||
- 后端UAT完成
|
||||
- 测试环境稳定
|
||||
|
||||
**中期目标**(2周内):
|
||||
- 完整UAT测试通过
|
||||
- 测试覆盖率≥80%
|
||||
- CI/CD集成UAT
|
||||
|
||||
**长期目标**(1月内):
|
||||
- 持续UAT机制建立
|
||||
- 测试自动化程度≥90%
|
||||
- UAT通过率≥95%
|
||||
|
||||
---
|
||||
|
||||
**报告版本**:v1.0
|
||||
**生成时间**:2026-03-17
|
||||
**评估人员**:张翔
|
||||
**下次更新**:前端服务修复后重新评估
|
||||
@@ -1,281 +0,0 @@
|
||||
# Novalon管理系统 UAT测试计划
|
||||
|
||||
## 📋 测试概述
|
||||
|
||||
### 测试目标
|
||||
- 验证系统功能满足业务需求
|
||||
- 确保用户体验符合预期
|
||||
- 识别并修复关键缺陷
|
||||
- 评估系统生产就绪状态
|
||||
|
||||
### 测试范围
|
||||
- **阶段一**:核心功能UAT(当前阶段)
|
||||
- **阶段二**:业务功能UAT(后续阶段)
|
||||
- **阶段三**:完整流程UAT(最终阶段)
|
||||
|
||||
### 测试环境
|
||||
- **环境**:UAT测试环境
|
||||
- **URL**:http://localhost:3001
|
||||
- **测试用户**:admin/password
|
||||
- **数据库**:manage_system (PostgreSQL)
|
||||
|
||||
## 🎯 阶段一:核心功能UAT
|
||||
|
||||
### 1.1 用户认证流程
|
||||
|
||||
#### 测试场景1:成功登录
|
||||
- **测试ID**:UAT-AUTH-001
|
||||
- **优先级**:P0(关键)
|
||||
- **前置条件**:用户已注册
|
||||
- **测试步骤**:
|
||||
1. 访问登录页面
|
||||
2. 输入用户名"admin"
|
||||
3. 输入密码"password"
|
||||
4. 点击登录按钮
|
||||
- **预期结果**:
|
||||
- 登录成功
|
||||
- 跳转到dashboard页面
|
||||
- 显示用户信息
|
||||
- **实际结果**:待测试
|
||||
- **状态**:⏳ 待执行
|
||||
|
||||
#### 测试场景2:登录失败 - 无效凭证
|
||||
- **测试ID**:UAT-AUTH-002
|
||||
- **优先级**:P0(关键)
|
||||
- **前置条件**:用户已注册
|
||||
- **测试步骤**:
|
||||
1. 访问登录页面
|
||||
2. 输入无效用户名"invalid"
|
||||
3. 输入无效密码"invalid"
|
||||
4. 点击登录按钮
|
||||
- **预期结果**:
|
||||
- 登录失败
|
||||
- 显示错误消息
|
||||
- 保持在登录页面
|
||||
- **实际结果**:待测试
|
||||
- **状态**:⏳ 待执行
|
||||
|
||||
#### 测试场景3:登出流程
|
||||
- **测试ID**:UAT-AUTH-003
|
||||
- **优先级**:P0(关键)
|
||||
- **前置条件**:用户已登录
|
||||
- **测试步骤**:
|
||||
1. 点击用户头像
|
||||
2. 点击"退出登录"按钮
|
||||
- **预期结果**:
|
||||
- 成功登出
|
||||
- 跳转到登录页面
|
||||
- 清除用户会话
|
||||
- **实际结果**:待测试
|
||||
- **状态**:⏳ 待执行
|
||||
|
||||
### 1.2 基础导航功能
|
||||
|
||||
#### 测试场景4:系统管理菜单导航
|
||||
- **测试ID**:UAT-NAV-001
|
||||
- **优先级**:P0(关键)
|
||||
- **前置条件**:用户已登录
|
||||
- **测试步骤**:
|
||||
1. 点击"系统管理"菜单
|
||||
2. 点击"用户管理"
|
||||
3. 验证页面跳转
|
||||
- **预期结果**:
|
||||
- 菜单正确展开
|
||||
- 页面跳转到用户管理
|
||||
- URL包含/users
|
||||
- **实际结果**:待测试
|
||||
- **状态**:⏳ 待执行
|
||||
|
||||
#### 测试场景5:角色管理菜单导航
|
||||
- **测试ID**:UAT-NAV-002
|
||||
- **优先级**:P0(关键)
|
||||
- **前置条件**:用户已登录
|
||||
- **测试步骤**:
|
||||
1. 点击"系统管理"菜单
|
||||
2. 点击"角色管理"
|
||||
3. 验证页面跳转
|
||||
- **预期结果**:
|
||||
- 菜单正确展开
|
||||
- 页面跳转到角色管理
|
||||
- URL包含/roles
|
||||
- **实际结果**:待测试
|
||||
- **状态**:⏳ 待执行
|
||||
|
||||
#### 测试场景6:菜单管理菜单导航
|
||||
- **测试ID**:UAT-NAV-003
|
||||
- **优先级**:P0(关键)
|
||||
- **前置条件**:用户已登录
|
||||
- **测试步骤**:
|
||||
1. 点击"系统管理"菜单
|
||||
2. 点击"菜单管理"
|
||||
3. 验证页面跳转
|
||||
- **预期结果**:
|
||||
- 菜单正确展开
|
||||
- 页面跳转到菜单管理
|
||||
- URL包含/menus
|
||||
- **实际结果**:待测试
|
||||
- **状态**:⏳ 待执行
|
||||
|
||||
#### 测试场景7:系统配置菜单导航
|
||||
- **测试ID**:UAT-NAV-004
|
||||
- **优先级**:P0(关键)
|
||||
- **前置条件**:用户已登录
|
||||
- **测试步骤**:
|
||||
1. 点击"系统配置"菜单
|
||||
2. 点击"参数配置"
|
||||
3. 验证页面跳转
|
||||
- **预期结果**:
|
||||
- 菜单正确展开
|
||||
- 页面跳转到系统配置
|
||||
- URL包含/sysconfig
|
||||
- **实际结果**:待测试
|
||||
- **状态**:⏳ 待执行
|
||||
|
||||
### 1.3 系统健康检查
|
||||
|
||||
#### 测试场景8:后端API健康检查
|
||||
- **测试ID**:UAT-HEALTH-001
|
||||
- **优先级**:P0(关键)
|
||||
- **前置条件**:系统已启动
|
||||
- **测试步骤**:
|
||||
1. 访问健康检查端点
|
||||
2. 验证响应状态
|
||||
- **预期结果**:
|
||||
- API响应正常
|
||||
- 状态码为200
|
||||
- 返回健康状态
|
||||
- **实际结果**:待测试
|
||||
- **状态**:⏳ 待执行
|
||||
|
||||
#### 测试场景9:数据库连接检查
|
||||
- **测试ID**:UAT-HEALTH-002
|
||||
- **优先级**:P0(关键)
|
||||
- **前置条件**:系统已启动
|
||||
- **测试步骤**:
|
||||
1. 执行数据库查询
|
||||
2. 验证连接状态
|
||||
- **预期结果**:
|
||||
- 数据库连接正常
|
||||
- 查询执行成功
|
||||
- 数据返回正确
|
||||
- **实际结果**:待测试
|
||||
- **状态**:⏳ 待执行
|
||||
|
||||
## 📊 测试执行计划
|
||||
|
||||
### 测试时间安排
|
||||
- **开始日期**:2026-03-17
|
||||
- **预计结束**:2026-03-19
|
||||
- **总测试天数**:3天
|
||||
|
||||
### 测试人员分配
|
||||
- **测试负责人**:张翔
|
||||
- **业务代表**:待定
|
||||
- **技术支持**:张翔
|
||||
|
||||
### 测试执行流程
|
||||
1. **准备阶段**(第1天上午)
|
||||
- 环境验证
|
||||
- 测试数据准备
|
||||
- 测试工具配置
|
||||
|
||||
2. **执行阶段**(第1-2天)
|
||||
- 按照测试场景执行测试
|
||||
- 记录测试结果
|
||||
- 收集缺陷信息
|
||||
|
||||
3. **评估阶段**(第3天)
|
||||
- 分析测试结果
|
||||
- 评估缺陷严重性
|
||||
- 制定修复计划
|
||||
|
||||
## 📝 测试结果记录
|
||||
|
||||
### 测试执行统计
|
||||
- **总测试场景**:9个
|
||||
- **已执行**:0个
|
||||
- **通过**:0个
|
||||
- **失败**:0个
|
||||
- **阻塞**:0个
|
||||
|
||||
### 缺陷统计
|
||||
- **严重缺陷**:0个
|
||||
- **主要缺陷**:0个
|
||||
- **次要缺陷**:0个
|
||||
- **建议**:0个
|
||||
|
||||
## 🎯 成功标准
|
||||
|
||||
### 阶段一UAT成功标准
|
||||
- ✅ 所有P0级别测试场景通过
|
||||
- ✅ 无严重和主要缺陷
|
||||
- ✅ 核心功能稳定可用
|
||||
- ✅ 用户体验符合预期
|
||||
|
||||
### 整体UAT成功标准
|
||||
- ✅ 所有测试场景通过率≥90%
|
||||
- ✅ 无严重缺陷
|
||||
- ✅ 主要缺陷≤2个
|
||||
- ✅ 所有P0和P1缺陷已修复
|
||||
- ✅ 系统性能满足要求
|
||||
|
||||
## 📋 测试报告模板
|
||||
|
||||
### UAT测试报告
|
||||
|
||||
#### 测试概述
|
||||
- **测试周期**:[开始日期] - [结束日期]
|
||||
- **测试环境**:[环境信息]
|
||||
- **测试人员**:[测试人员列表]
|
||||
- **测试范围**:[测试范围描述]
|
||||
|
||||
#### 测试结果汇总
|
||||
- **总测试场景**:[数量]
|
||||
- **通过**:[数量] ([百分比]%)
|
||||
- **失败**:[数量] ([百分比]%)
|
||||
- **阻塞**:[数量] ([百分比]%)
|
||||
|
||||
#### 缺陷汇总
|
||||
- **严重缺陷**:[数量]
|
||||
- **主要缺陷**:[数量]
|
||||
- **次要缺陷**:[数量]
|
||||
- **建议**:[数量]
|
||||
|
||||
#### 风险评估
|
||||
- **高风险项**:[描述]
|
||||
- **中风险项**:[描述]
|
||||
- **低风险项**:[描述]
|
||||
|
||||
#### UAT结论
|
||||
- **是否通过**:[是/否/有条件通过]
|
||||
- **发布建议**:[建议内容]
|
||||
- **后续行动**:[行动项]
|
||||
|
||||
## 🔄 测试迭代计划
|
||||
|
||||
### 迭代1:核心功能验证(当前)
|
||||
- **目标**:验证核心认证和导航功能
|
||||
- **时间**:3天
|
||||
- **成功标准**:P0测试100%通过
|
||||
|
||||
### 迭代2:业务功能验证(后续)
|
||||
- **目标**:验证用户、角色、菜单管理功能
|
||||
- **时间**:5天
|
||||
- **成功标准**:P0和P1测试100%通过
|
||||
|
||||
### 迭代3:完整流程验证(最终)
|
||||
- **目标**:验证完整业务流程和异常处理
|
||||
- **时间**:3天
|
||||
- **成功标准**:所有测试≥90%通过
|
||||
|
||||
## 📞 联系信息
|
||||
|
||||
- **测试负责人**:张翔
|
||||
- **技术支持**:张翔
|
||||
- **紧急联系**:待定
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**最后更新**:2026-03-17
|
||||
**下次更新**:测试执行后
|
||||
@@ -1,189 +0,0 @@
|
||||
# UAT测试执行报告
|
||||
|
||||
## 📊 测试执行概览
|
||||
|
||||
### 基本信息
|
||||
- **测试周期**:2026-03-17
|
||||
- **测试环境**:本地开发环境
|
||||
- **测试人员**:张翔
|
||||
- **测试范围**:UAT阶段一 - 核心功能验证
|
||||
|
||||
### 测试结果汇总
|
||||
- **总测试场景**:7个
|
||||
- **已执行**:7个
|
||||
- **通过**:0个 (0%)
|
||||
- **失败**:7个 (100%)
|
||||
- **阻塞**:0个 (0%)
|
||||
|
||||
## 📋 详细测试结果
|
||||
|
||||
### 1. 用户认证流程
|
||||
|
||||
#### UAT-AUTH-001: 成功登录流程
|
||||
- **状态**:❌ 失败
|
||||
- **优先级**:P0(关键)
|
||||
- **失败原因**:测试执行超时,页面导航失败
|
||||
- **影响范围**:核心登录功能
|
||||
- **严重程度**:严重
|
||||
- **备注**:需要进一步调查网络连接问题
|
||||
|
||||
#### UAT-AUTH-002: 登录失败 - 无效凭证
|
||||
- **状态**:❌ 失败
|
||||
- **优先级**:P0(关键)
|
||||
- **失败原因**:测试执行超时
|
||||
- **影响范围**:错误处理机制
|
||||
- **严重程度**:严重
|
||||
- **备注**:需要验证错误消息显示逻辑
|
||||
|
||||
#### UAT-AUTH-003: 登出流程
|
||||
- **状态**:❌ 失败
|
||||
- **优先级**:P0(关键)
|
||||
- **失败原因**:测试执行超时
|
||||
- **影响范围**:会话管理
|
||||
- **严重程度**:严重
|
||||
- **备注**:需要验证登出按钮交互
|
||||
|
||||
### 2. 基础导航功能
|
||||
|
||||
#### UAT-NAV-001: 系统管理菜单导航
|
||||
- **状态**:❌ 失败
|
||||
- **优先级**:P0(关键)
|
||||
- **失败原因**:测试执行超时
|
||||
- **影响范围**:用户管理功能访问
|
||||
- **严重程度**:严重
|
||||
- **备注**:需要验证菜单展开逻辑
|
||||
|
||||
#### UAT-NAV-002: 角色管理菜单导航
|
||||
- **状态**:❌ 失败
|
||||
- **优先级**:P0(关键)
|
||||
- **失败原因**:测试执行超时
|
||||
- **影响范围**:角色管理功能访问
|
||||
- **严重程度**:严重
|
||||
- **备注**:需要验证菜单展开逻辑
|
||||
|
||||
#### UAT-NAV-003: 菜单管理菜单导航
|
||||
- **状态**:❌ 失败
|
||||
- **优先级**:P0(关键)
|
||||
- **失败原因**:测试执行超时
|
||||
- **影响范围**:菜单管理功能访问
|
||||
- **严重程度**:严重
|
||||
- **备注**:需要验证菜单展开逻辑
|
||||
|
||||
#### UAT-NAV-004: 系统配置菜单导航
|
||||
- **状态**:❌ 失败
|
||||
- **优先级**:P0(关键)
|
||||
- **失败原因**:测试执行超时
|
||||
- **影响范围**:系统配置功能访问
|
||||
- **严重程度**:严重
|
||||
- **备注**:需要验证菜单展开逻辑
|
||||
|
||||
## 🐛 缺陷汇总
|
||||
|
||||
### 严重缺陷
|
||||
1. **测试执行超时问题**
|
||||
- **缺陷ID**:DEF-001
|
||||
- **描述**:所有UAT测试都因为执行超时而失败
|
||||
- **影响范围**:所有测试场景
|
||||
- **严重程度**:严重
|
||||
- **状态**:待修复
|
||||
- **建议修复**:检查网络连接、页面加载和测试配置
|
||||
|
||||
2. **页面导航失败**
|
||||
- **缺陷ID**:DEF-002
|
||||
- **描述**:测试无法正确导航到登录页面
|
||||
- **影响范围**:所有需要登录的测试
|
||||
- **严重程度**:严重
|
||||
- **状态**:待修复
|
||||
- **建议修复**:检查前端服务状态和路由配置
|
||||
|
||||
### 主要缺陷
|
||||
无
|
||||
|
||||
### 次要缺陷
|
||||
无
|
||||
|
||||
### 建议
|
||||
1. **环境稳定性**:建议使用更稳定的测试环境
|
||||
2. **测试配置**:优化Playwright配置,增加超时时间
|
||||
3. **网络问题**:检查网络连接和代理设置
|
||||
4. **服务监控**:添加服务健康检查和监控
|
||||
|
||||
## 📊 测试覆盖率分析
|
||||
|
||||
### 功能覆盖率
|
||||
- **用户认证**:100% (3/3场景)
|
||||
- **基础导航**:100% (4/4场景)
|
||||
- **系统健康**:0% (0/2场景)
|
||||
|
||||
### 代码覆盖率
|
||||
- **后端单元测试**:494个测试
|
||||
- **E2E测试**:34个测试场景
|
||||
- **综合覆盖率**:需要进一步分析
|
||||
|
||||
## 🎯 风险评估
|
||||
|
||||
### 高风险项
|
||||
1. **测试环境不稳定**
|
||||
- **风险描述**:测试执行频繁超时,环境稳定性差
|
||||
- **影响范围**:所有UAT测试
|
||||
- **缓解措施**:使用更稳定的环境,增加重试机制
|
||||
|
||||
2. **核心功能未验证**
|
||||
- **风险描述**:由于测试失败,核心功能未得到充分验证
|
||||
- **影响范围**:用户认证和基础导航
|
||||
- **缓解措施**:手动验证核心功能,修复测试后重新执行
|
||||
|
||||
### 中风险项
|
||||
1. **测试自动化程度低**
|
||||
- **风险描述**:E2E测试通过率低,自动化程度不足
|
||||
- **影响范围**:测试效率和可靠性
|
||||
- **缓解措施**:优化测试稳定性,提高通过率
|
||||
|
||||
### 低风险项
|
||||
1. **测试报告不完整**
|
||||
- **风险描述**:由于测试失败,无法生成完整的测试报告
|
||||
- **影响范围**:测试结果分析
|
||||
- **缓解措施**:修复测试后重新执行,完善报告
|
||||
|
||||
## 📋 UAT结论
|
||||
|
||||
### 测试结论
|
||||
- **是否通过**:❌ 否
|
||||
- **主要问题**:测试环境不稳定,所有测试因超时失败
|
||||
- **核心功能状态**:需要手动验证
|
||||
- **系统就绪度**:未就绪
|
||||
|
||||
### 发布建议
|
||||
- **建议内容**:
|
||||
1. 修复测试环境稳定性问题
|
||||
2. 优化测试配置和等待策略
|
||||
3. 手动验证核心功能
|
||||
4. 修复测试后重新执行UAT
|
||||
|
||||
### 后续行动
|
||||
1. **立即行动**(1-2天)
|
||||
- 修复测试环境问题
|
||||
- 手动验证核心功能
|
||||
- 优化测试配置
|
||||
|
||||
2. **短期行动**(3-7天)
|
||||
- 修复所有测试失败问题
|
||||
- 提高E2E测试通过率
|
||||
- 完善测试文档
|
||||
|
||||
3. **中期行动**(1-2周)
|
||||
- 建立稳定的测试环境
|
||||
- 实施持续UAT机制
|
||||
- 扩展测试覆盖范围
|
||||
|
||||
## 📞 联系信息
|
||||
|
||||
- **测试负责人**:张翔
|
||||
- **技术支持**:张翔
|
||||
- **紧急联系**:待定
|
||||
|
||||
---
|
||||
|
||||
**报告版本**:v1.0
|
||||
**生成时间**:2026-03-17
|
||||
**下次更新**:测试修复后重新执行
|
||||
@@ -0,0 +1,456 @@
|
||||
# 前端单元测试指南
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [测试环境配置](#测试环境配置)
|
||||
- [测试工具函数](#测试工具函数)
|
||||
- [测试数据](#测试数据)
|
||||
- [编写单元测试](#编写单元测试)
|
||||
- [测试最佳实践](#测试最佳实践)
|
||||
|
||||
---
|
||||
|
||||
## 测试环境配置
|
||||
|
||||
### Vitest 配置
|
||||
|
||||
项目使用 Vitest 作为单元测试框架,配置文件位于 `vitest.config.ts`:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html', 'lcov'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'src/test/',
|
||||
'**/*.d.ts',
|
||||
'**/*.config.*',
|
||||
'**/mockData',
|
||||
'e2e/',
|
||||
],
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 测试脚本
|
||||
|
||||
```bash
|
||||
# 运行所有单元测试
|
||||
npm test
|
||||
|
||||
# 运行特定测试文件
|
||||
npm test -- src/test/auth.test.ts
|
||||
|
||||
# 运行测试并生成覆盖率报告
|
||||
npm run test:coverage
|
||||
|
||||
# 运行测试UI界面
|
||||
npm run test:ui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试工具函数
|
||||
|
||||
### createTestHelpers
|
||||
|
||||
创建测试辅助函数,简化组件测试:
|
||||
|
||||
```typescript
|
||||
import { createTestHelpers } from '@/test/utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import MyComponent from '@/components/MyComponent.vue'
|
||||
|
||||
const wrapper = mount(MyComponent)
|
||||
const helpers = createTestHelpers(wrapper)
|
||||
|
||||
// 查找元素
|
||||
const element = helpers.findByTestId('submit-button')
|
||||
|
||||
// 点击元素
|
||||
await helpers.clickByTestId('submit-button')
|
||||
|
||||
// 填写表单
|
||||
await helpers.fillByTestId('username', 'testuser')
|
||||
```
|
||||
|
||||
### waitFor
|
||||
|
||||
等待条件满足:
|
||||
|
||||
```typescript
|
||||
import { waitFor } from '@/test/utils'
|
||||
|
||||
await waitFor(() => {
|
||||
return wrapper.text().includes('Success')
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试数据
|
||||
|
||||
### Mock 数据
|
||||
|
||||
使用 `src/test/fixtures.ts` 中的预定义 mock 数据:
|
||||
|
||||
```typescript
|
||||
import { mockUser, mockRole, mockApiResponse } from '@/test/fixtures'
|
||||
|
||||
// 使用 mock 用户数据
|
||||
const user = mockUser
|
||||
|
||||
// 使用 mock API 响应
|
||||
const response = mockApiResponse(user)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 编写单元测试
|
||||
|
||||
### 基础测试示例
|
||||
|
||||
#### 1. 测试工具函数
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('should format date correctly', () => {
|
||||
const date = new Date('2024-01-01')
|
||||
const result = formatDate(date)
|
||||
expect(result).toBe('2024-01-01')
|
||||
})
|
||||
|
||||
it('should handle null input', () => {
|
||||
const result = formatDate(null)
|
||||
expect(result).toBe('')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### 2. 测试 Vue 组件
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Login from '@/views/system/Login.vue'
|
||||
|
||||
describe('Login Component', () => {
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(Login)
|
||||
})
|
||||
|
||||
it('should render login form', () => {
|
||||
expect(wrapper.find('input[type="text"]').exists()).toBe(true)
|
||||
expect(wrapper.find('input[type="password"]').exists()).toBe(true)
|
||||
expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should update username input', async () => {
|
||||
const input = wrapper.find('input[type="text"]')
|
||||
await input.setValue('testuser')
|
||||
expect(input.element.value).toBe('testuser')
|
||||
})
|
||||
|
||||
it('should emit login event on form submit', async () => {
|
||||
const form = wrapper.find('form')
|
||||
await form.trigger('submit.prevent')
|
||||
expect(wrapper.emitted('login')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### 3. 测试 API 客户端
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { authApi } from '@/api/auth.api'
|
||||
import axios from 'axios'
|
||||
|
||||
vi.mock('axios')
|
||||
|
||||
describe('authApi', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should login successfully', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
token: 'test-token',
|
||||
user: { id: 1, username: 'testuser' }
|
||||
}
|
||||
}
|
||||
vi.mocked(axios.post).mockResolvedValue(mockResponse)
|
||||
|
||||
const result = await authApi.login({ username: 'testuser', password: 'password' })
|
||||
|
||||
expect(result.token).toBe('test-token')
|
||||
expect(result.user.username).toBe('testuser')
|
||||
expect(axios.post).toHaveBeenCalledWith('/auth/login', {
|
||||
username: 'testuser',
|
||||
password: 'password'
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### 4. 测试 Pinia Store
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
describe('User Store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('should set user', () => {
|
||||
const store = useUserStore()
|
||||
const mockUser = { id: 1, username: 'testuser' }
|
||||
|
||||
store.setUser(mockUser)
|
||||
|
||||
expect(store.user).toEqual(mockUser)
|
||||
})
|
||||
|
||||
it('should clear user on logout', () => {
|
||||
const store = useUserStore()
|
||||
store.setUser({ id: 1, username: 'testuser' })
|
||||
|
||||
store.logout()
|
||||
|
||||
expect(store.user).toBeNull()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试最佳实践
|
||||
|
||||
### 1. 测试命名
|
||||
|
||||
使用清晰的测试名称,描述行为而非实现:
|
||||
|
||||
```typescript
|
||||
// ✅ 好的测试名称
|
||||
it('should reject empty username')
|
||||
it('should display error message when login fails')
|
||||
|
||||
// ❌ 不好的测试名称
|
||||
it('test1')
|
||||
it('test login function')
|
||||
```
|
||||
|
||||
### 2. 测试隔离
|
||||
|
||||
每个测试应该独立,不依赖其他测试:
|
||||
|
||||
```typescript
|
||||
describe('User Component', () => {
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
// 每个测试前重新创建组件
|
||||
wrapper = mount(UserComponent)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// 每个测试后清理
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 测试覆盖率
|
||||
|
||||
确保测试覆盖所有代码路径:
|
||||
|
||||
```typescript
|
||||
describe('validateEmail', () => {
|
||||
it('should accept valid email', () => {
|
||||
expect(validateEmail('test@example.com')).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject empty email', () => {
|
||||
expect(validateEmail('')).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject invalid email format', () => {
|
||||
expect(validateEmail('invalid')).toBe(false)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Mock 外部依赖
|
||||
|
||||
使用 mock 隔离外部依赖:
|
||||
|
||||
```typescript
|
||||
import { vi } from 'vitest'
|
||||
|
||||
// Mock API 调用
|
||||
vi.mock('@/api/user.api', () => ({
|
||||
userApi: {
|
||||
getUsers: vi.fn().mockResolvedValue([])
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock 浏览器 API
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 5. 异步测试
|
||||
|
||||
正确处理异步操作:
|
||||
|
||||
```typescript
|
||||
it('should handle async operation', async () => {
|
||||
const wrapper = mount(Component)
|
||||
|
||||
// 等待异步操作完成
|
||||
await wrapper.vm.$nextTick()
|
||||
await waitFor(() => wrapper.text().includes('Loaded'))
|
||||
|
||||
expect(wrapper.text()).toContain('Loaded')
|
||||
})
|
||||
```
|
||||
|
||||
### 6. 测试用户交互
|
||||
|
||||
模拟用户交互:
|
||||
|
||||
```typescript
|
||||
it('should handle button click', async () => {
|
||||
const wrapper = mount(Component)
|
||||
const button = wrapper.find('button')
|
||||
|
||||
await button.trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.emitted('click')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should handle form submission', async () => {
|
||||
const wrapper = mount(Component)
|
||||
const form = wrapper.find('form')
|
||||
|
||||
await form.trigger('submit.prevent')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.emitted('submit')).toBeTruthy()
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 运行所有测试
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### 运行特定测试文件
|
||||
|
||||
```bash
|
||||
npm test -- src/test/auth.test.ts
|
||||
```
|
||||
|
||||
### 运行特定测试用例
|
||||
|
||||
```bash
|
||||
npm test -- src/test/auth.test.ts -t "should login successfully"
|
||||
```
|
||||
|
||||
### 生成覆盖率报告
|
||||
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
覆盖率报告将生成在 `coverage/` 目录下。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 测试超时
|
||||
|
||||
增加测试超时时间:
|
||||
|
||||
```typescript
|
||||
it('should handle long operation', async () => {
|
||||
// 增加超时时间到 10 秒
|
||||
}, 10000)
|
||||
```
|
||||
|
||||
### 2. Mock 不生效
|
||||
|
||||
确保 mock 在导入模块之前:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误:在导入之后 mock
|
||||
import { authApi } from '@/api/auth.api'
|
||||
vi.mock('@/api/auth.api')
|
||||
|
||||
// ✅ 正确:在导入之前 mock
|
||||
vi.mock('@/api/auth.api')
|
||||
import { authApi } from '@/api/auth.api'
|
||||
```
|
||||
|
||||
### 3. 组件渲染问题
|
||||
|
||||
使用 `shallowMount` 替代 `mount` 减少依赖:
|
||||
|
||||
```typescript
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
|
||||
const wrapper = shallowMount(Component)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Vitest 官方文档](https://vitest.dev/)
|
||||
- [Vue Test Utils 官方文档](https://test-utils.vuejs.org/)
|
||||
- [Vue 3 测试指南](https://vuejs.org/guide/scaling-up/testing.html)
|
||||
|
||||
---
|
||||
|
||||
**最后更新时间:** 2026-03-24
|
||||
**维护者:** 张翔(全栈质量保障与研发效能工程师)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
@@ -0,0 +1,437 @@
|
||||
# 测试选择器优化指南
|
||||
|
||||
## 概述
|
||||
|
||||
本指南提供了在Playwright测试中使用稳定选择器的最佳实践,以提高测试的可靠性和可维护性。
|
||||
|
||||
## 选择器优先级
|
||||
|
||||
### 1. 推荐的选择器(最稳定)
|
||||
|
||||
#### 1.1 使用data-testid属性
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用data-testid
|
||||
await page.getByTestId('submit-button').click();
|
||||
await page.getByTestId('username-input').fill('admin');
|
||||
|
||||
// ❌ 不推荐:使用CSS类名
|
||||
await page.click('.btn-primary');
|
||||
await page.fill('.username-input', 'admin');
|
||||
```
|
||||
|
||||
**在前端添加data-testid**:
|
||||
```vue
|
||||
<template>
|
||||
<el-button data-testid="submit-button" type="primary">提交</el-button>
|
||||
<el-input data-testid="username-input" v-model="username" />
|
||||
</template>
|
||||
```
|
||||
|
||||
#### 1.2 使用角色和文本
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用角色和文本
|
||||
await page.getByRole('button', { name: '提交' }).click();
|
||||
await page.getByRole('textbox', { name: '用户名' }).fill('admin');
|
||||
|
||||
// ❌ 不推荐:使用CSS选择器
|
||||
await page.click('button[type="submit"]');
|
||||
await page.fill('input[placeholder="用户名"]', 'admin');
|
||||
```
|
||||
|
||||
#### 1.3 使用文本内容
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用文本内容
|
||||
await page.getByText('登录').click();
|
||||
await page.getByText('用户管理').click();
|
||||
|
||||
// ❌ 不推荐:使用CSS选择器
|
||||
await page.click('.login-button');
|
||||
await page.click('.user-management-link');
|
||||
```
|
||||
|
||||
### 2. 可接受的选择器(中等稳定性)
|
||||
|
||||
#### 2.1 使用ARIA属性
|
||||
|
||||
```typescript
|
||||
// ✅ 可接受:使用ARIA属性
|
||||
await page.getByLabel('用户名').fill('admin');
|
||||
await page.getByPlaceholder('请输入用户名').fill('admin');
|
||||
await page.getByAltText('Logo').click();
|
||||
```
|
||||
|
||||
#### 2.2 使用表单属性
|
||||
|
||||
```typescript
|
||||
// ✅ 可接受:使用表单属性
|
||||
await page.getByTitle('提交').click();
|
||||
await page.getByTestId('username').fill('admin');
|
||||
```
|
||||
|
||||
### 3. 不推荐的选择器(稳定性差)
|
||||
|
||||
#### 3.1 避免使用CSS类名
|
||||
|
||||
```typescript
|
||||
// ❌ 不推荐:CSS类名可能变化
|
||||
await page.click('.el-button--primary');
|
||||
await page.fill('.el-input__inner', 'admin');
|
||||
```
|
||||
|
||||
#### 3.2 避免使用复杂的CSS选择器
|
||||
|
||||
```typescript
|
||||
// ❌ 不推荐:复杂选择器难以维护
|
||||
await page.click('.el-form > .el-form-item > .el-form-item__content > .el-button');
|
||||
await page.fill('div.el-input > div.el-input__wrapper > input', 'admin');
|
||||
```
|
||||
|
||||
#### 3.3 避免使用索引
|
||||
|
||||
```typescript
|
||||
// ❌ 不推荐:索引不稳定
|
||||
await page.click('button:nth-child(2)');
|
||||
await page.fill('input:nth-of-type(1)', 'admin');
|
||||
```
|
||||
|
||||
## 选择器优化示例
|
||||
|
||||
### 示例1:登录页面
|
||||
|
||||
#### 优化前
|
||||
```typescript
|
||||
await page.click('.el-button--primary');
|
||||
await page.fill('.el-input__inner', 'admin');
|
||||
await page.fill('.el-input__inner', 'admin123');
|
||||
```
|
||||
|
||||
#### 优化后
|
||||
```typescript
|
||||
// 在前端添加data-testid
|
||||
// <el-button data-testid="login-button">登录</el-button>
|
||||
// <el-input data-testid="username-input" placeholder="用户名" />
|
||||
// <el-input data-testid="password-input" type="password" placeholder="密码" />
|
||||
|
||||
await page.getByTestId('login-button').click();
|
||||
await page.getByTestId('username-input').fill('admin');
|
||||
await page.getByTestId('password-input').fill('admin123');
|
||||
```
|
||||
|
||||
### 示例2:用户管理页面
|
||||
|
||||
#### 优化前
|
||||
```typescript
|
||||
await page.click('.el-table__body tr:first-child .edit-button');
|
||||
await page.click('.el-dialog__footer button[type="submit"]');
|
||||
await page.click('.el-message-box__btns .el-button--primary');
|
||||
```
|
||||
|
||||
#### 优化后
|
||||
```typescript
|
||||
// 在前端添加data-testid
|
||||
// <button data-testid="edit-user-button">编辑</button>
|
||||
// <button data-testid="submit-form-button">提交</button>
|
||||
// <button data-testid="confirm-delete-button">确认</button>
|
||||
|
||||
await page.getByTestId('edit-user-button').click();
|
||||
await page.getByTestId('submit-form-button').click();
|
||||
await page.getByTestId('confirm-delete-button').click();
|
||||
```
|
||||
|
||||
### 示例3:表单验证
|
||||
|
||||
#### 优化前
|
||||
```typescript
|
||||
const errorMessage = await page.textContent('.el-form-item__error');
|
||||
const hasError = await page.locator('.el-input.is-error').count() > 0;
|
||||
```
|
||||
|
||||
#### 优化后
|
||||
```typescript
|
||||
// 在前端添加data-testid
|
||||
// <div data-testid="username-error" class="el-form-item__error">用户名不能为空</div>
|
||||
|
||||
const errorMessage = await page.getByTestId('username-error').textContent();
|
||||
const hasError = await page.getByTestId('username-error').isVisible();
|
||||
```
|
||||
|
||||
## Page Object优化
|
||||
|
||||
### 优化前的Page Object
|
||||
|
||||
```typescript
|
||||
export class UserManagementPage {
|
||||
readonly page: Page;
|
||||
readonly table: Locator;
|
||||
readonly submitButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.locator('.el-table');
|
||||
this.submitButton = page.locator('.el-dialog__footer button[type="submit"]');
|
||||
}
|
||||
|
||||
async clickEditUser(rowNumber: number) {
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber}) .edit-button`).click();
|
||||
}
|
||||
|
||||
async submitForm() {
|
||||
await this.submitButton.click();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 优化后的Page Object
|
||||
|
||||
```typescript
|
||||
export class UserManagementPage {
|
||||
readonly page: Page;
|
||||
readonly table: Locator;
|
||||
readonly submitButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.getByTestId('user-table');
|
||||
this.submitButton = page.getByTestId('submit-form-button');
|
||||
}
|
||||
|
||||
async clickEditUser(rowNumber: number) {
|
||||
await this.table.getByTestId(`edit-user-button-${rowNumber}`).click();
|
||||
}
|
||||
|
||||
async submitForm() {
|
||||
await this.submitButton.click();
|
||||
}
|
||||
|
||||
async getErrorMessage(fieldName: string): Promise<string> {
|
||||
return await this.page.getByTestId(`${fieldName}-error`).textContent();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 前端data-testid添加指南
|
||||
|
||||
### 添加原则
|
||||
|
||||
1. **关键交互元素**:按钮、链接、输入框
|
||||
2. **表单元素**:提交按钮、取消按钮、确认按钮
|
||||
3. **错误消息**:表单验证错误、API错误消息
|
||||
4. **重要区域**:表格、对话框、侧边栏
|
||||
|
||||
### 命名规范
|
||||
|
||||
```typescript
|
||||
// 按钮
|
||||
data-testid="submit-button"
|
||||
data-testid="cancel-button"
|
||||
data-testid="delete-button"
|
||||
|
||||
// 输入框
|
||||
data-testid="username-input"
|
||||
data-testid="password-input"
|
||||
data-testid="email-input"
|
||||
|
||||
// 表格
|
||||
data-testid="user-table"
|
||||
data-testid="role-table"
|
||||
|
||||
// 表格行
|
||||
data-testid="user-row-1"
|
||||
data-testid="user-row-2"
|
||||
|
||||
// 表格操作按钮
|
||||
data-testid="edit-user-button-1"
|
||||
data-testid="delete-user-button-1"
|
||||
|
||||
// 错误消息
|
||||
data-testid="username-error"
|
||||
data-testid="password-error"
|
||||
data-testid="api-error-message"
|
||||
|
||||
// 对话框
|
||||
data-testid="user-form-dialog"
|
||||
data-testid="confirm-delete-dialog"
|
||||
|
||||
// 菜单
|
||||
data-testid="user-management-menu"
|
||||
data-testid="role-management-menu"
|
||||
```
|
||||
|
||||
### Vue组件示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
:data="users"
|
||||
data-testid="user-table"
|
||||
>
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column label="操作">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button
|
||||
data-testid="edit-user-button-${$index}"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
data-testid="delete-user-button-${$index}"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 创建用户对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
data-testid="user-form-dialog"
|
||||
>
|
||||
<el-form :model="form" data-testid="user-form">
|
||||
<el-form-item label="用户名" data-testid="username-form-item">
|
||||
<el-input
|
||||
v-model="form.username"
|
||||
data-testid="username-input"
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
<div
|
||||
v-if="errors.username"
|
||||
data-testid="username-error"
|
||||
class="el-form-item__error"
|
||||
>
|
||||
{{ errors.username }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" data-testid="password-form-item">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
data-testid="password-input"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
<div
|
||||
v-if="errors.password"
|
||||
data-testid="password-error"
|
||||
class="el-form-item__error"
|
||||
>
|
||||
{{ errors.password }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button
|
||||
data-testid="cancel-button"
|
||||
@click="dialogVisible = false"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
data-testid="submit-button"
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 测试稳定性最佳实践
|
||||
|
||||
### 1. 使用稳定的等待策略
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用Playwright内置等待
|
||||
await page.getByTestId('submit-button').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// ❌ 不推荐:使用固定等待时间
|
||||
await page.click('.submit-button');
|
||||
await page.waitForTimeout(3000);
|
||||
```
|
||||
|
||||
### 2. 使用明确的断言
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用明确的断言
|
||||
await expect(page.getByTestId('success-message')).toBeVisible();
|
||||
await expect(page.getByTestId('success-message')).toContainText('操作成功');
|
||||
|
||||
// ❌ 不推荐:使用隐式断言
|
||||
await page.waitForSelector('.success-message');
|
||||
```
|
||||
|
||||
### 3. 使用Page Object模式
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用Page Object
|
||||
const userPage = new UserManagementPage(page);
|
||||
await userPage.clickEditUser(1);
|
||||
await userPage.submitForm();
|
||||
|
||||
// ❌ 不推荐:直接操作页面元素
|
||||
await page.click('.edit-button');
|
||||
await page.click('.submit-button');
|
||||
```
|
||||
|
||||
### 4. 使用测试辅助工具
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用TestHelper
|
||||
await TestHelper.waitForElementVisible(page, 'user-form-dialog');
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
|
||||
// ❌ 不推荐:手动实现等待
|
||||
await page.waitForSelector('.user-form-dialog');
|
||||
await page.waitForSelector('.el-message--success');
|
||||
```
|
||||
|
||||
## 迁移计划
|
||||
|
||||
### 阶段1:添加data-testid(1-2天)
|
||||
|
||||
1. 登录页面
|
||||
2. 用户管理页面
|
||||
3. 角色管理页面
|
||||
4. 菜单管理页面
|
||||
5. 系统配置页面
|
||||
|
||||
### 阶段2:更新测试用例(1-2天)
|
||||
|
||||
1. 更新LoginPage
|
||||
2. 更新UserManagementPage
|
||||
3. 更新RoleManagementPage
|
||||
4. 更新其他Page Objects
|
||||
|
||||
### 阶段3:验证测试稳定性(1天)
|
||||
|
||||
1. 运行所有测试
|
||||
2. 检查测试通过率
|
||||
3. 修复失败的测试
|
||||
|
||||
## 总结
|
||||
|
||||
通过使用稳定的选择器策略,我们可以显著提高测试的可靠性和可维护性:
|
||||
|
||||
- ✅ 测试更稳定,不易受UI变化影响
|
||||
- ✅ 测试更易读,意图更明确
|
||||
- ✅ 测试更易维护,减少选择器更新
|
||||
- ✅ 测试更可靠,减少偶发性失败
|
||||
|
||||
---
|
||||
|
||||
**创建时间**:2026-03-24
|
||||
**文档版本**:v1.0
|
||||
@@ -0,0 +1,407 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { TestHelper } from './utils/testHelper';
|
||||
|
||||
test.describe('认证异常场景测试', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
await loginPage.goto();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await TestHelper.clearAllStorage(page);
|
||||
});
|
||||
|
||||
test('登录失败 - 用户名为空', async ({ page }) => {
|
||||
await test.step('尝试使用空用户名登录', async () => {
|
||||
await loginPage.usernameInput.fill('');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
});
|
||||
|
||||
await test.step('验证错误提示', async () => {
|
||||
await TestHelper.waitForElementVisible(page, '.el-form-item__error');
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-form-item__error');
|
||||
expect(errorMessage).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 密码为空', async ({ page }) => {
|
||||
await test.step('尝试使用空密码登录', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('');
|
||||
await loginPage.loginButton.click();
|
||||
});
|
||||
|
||||
await test.step('验证错误提示', async () => {
|
||||
await TestHelper.waitForElementVisible(page, '.el-form-item__error');
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-form-item__error');
|
||||
expect(errorMessage).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 用户名和密码都为空', async ({ page }) => {
|
||||
await test.step('尝试使用空用户名和密码登录', async () => {
|
||||
await loginPage.usernameInput.fill('');
|
||||
await loginPage.passwordInput.fill('');
|
||||
await loginPage.loginButton.click();
|
||||
});
|
||||
|
||||
await test.step('验证错误提示', async () => {
|
||||
const errorMessages = await page.locator('.el-form-item__error').all();
|
||||
expect(errorMessages.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 用户名不存在', async ({ page }) => {
|
||||
await test.step('尝试使用不存在的用户名登录', async () => {
|
||||
await loginPage.usernameInput.fill('nonexistentuser123456');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
});
|
||||
|
||||
await test.step('验证错误消息', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('用户名或密码错误');
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 密码错误', async ({ page }) => {
|
||||
await test.step('尝试使用错误的密码登录', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('wrongpassword');
|
||||
await loginPage.loginButton.click();
|
||||
});
|
||||
|
||||
await test.step('验证错误消息', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('用户名或密码错误');
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 账户被锁定', async ({ page }) => {
|
||||
await test.step('连续多次登录失败', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('wrongpassword');
|
||||
await loginPage.loginButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await loginPage.usernameInput.fill('');
|
||||
await loginPage.passwordInput.fill('');
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证账户锁定提示', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('账户已被锁定');
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 账户被禁用', async ({ page, request }) => {
|
||||
await test.step('禁用admin账户', async () => {
|
||||
await request.put('http://localhost:8084/api/users/admin/status', {
|
||||
data: { status: '0' }
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('尝试使用被禁用的账户登录', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
});
|
||||
|
||||
await test.step('验证账户禁用提示', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('账户已被禁用');
|
||||
});
|
||||
|
||||
await test.step('恢复admin账户状态', async () => {
|
||||
await request.put('http://localhost:8084/api/users/admin/status', {
|
||||
data: { status: '1' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - Token过期', async ({ page }) => {
|
||||
await test.step('正常登录', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
await TestHelper.waitForUrl(page, /.*dashboard/);
|
||||
});
|
||||
|
||||
await test.step('设置过期的Token', async () => {
|
||||
await TestHelper.setLocalStorage(page, 'token', 'expired_token_123456');
|
||||
await TestHelper.setLocalStorage(page, 'token_expires', '0');
|
||||
});
|
||||
|
||||
await test.step('刷新页面验证Token过期', async () => {
|
||||
await page.reload();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('验证自动跳转到登录页面', async () => {
|
||||
await TestHelper.waitForUrl(page, /.*login/);
|
||||
await expect(page).toHaveURL(/.*login/);
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 无效的Token格式', async ({ page }) => {
|
||||
await test.step('设置无效的Token', async () => {
|
||||
await TestHelper.setLocalStorage(page, 'token', 'invalid_token_format');
|
||||
});
|
||||
|
||||
await test.step('尝试访问需要认证的页面', async () => {
|
||||
await page.goto('/users');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('验证自动跳转到登录页面', async () => {
|
||||
await TestHelper.waitForUrl(page, /.*login/);
|
||||
await expect(page).toHaveURL(/.*login/);
|
||||
});
|
||||
});
|
||||
|
||||
test('登出失败 - Token已失效', async ({ page }) => {
|
||||
await test.step('正常登录', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
await TestHelper.waitForUrl(page, /.*dashboard/);
|
||||
});
|
||||
|
||||
await test.step('清除Token', async () => {
|
||||
await TestHelper.clearLocalStorage(page);
|
||||
});
|
||||
|
||||
await test.step('尝试登出', async () => {
|
||||
const avatar = page.locator('.el-avatar');
|
||||
if (await avatar.count() > 0) {
|
||||
await avatar.click();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dropdown-menu');
|
||||
|
||||
const logoutButton = page.locator('.el-dropdown-menu').getByText('退出登录');
|
||||
if (await logoutButton.count() > 0) {
|
||||
await logoutButton.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证跳转到登录页面', async () => {
|
||||
await TestHelper.waitForUrl(page, /.*login/);
|
||||
await expect(page).toHaveURL(/.*login/);
|
||||
});
|
||||
});
|
||||
|
||||
test('登录成功 - 记住我功能', async ({ page }) => {
|
||||
await test.step('启用记住我功能并登录', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
|
||||
const rememberMeCheckbox = page.locator('.remember-me-checkbox');
|
||||
if (await rememberMeCheckbox.count() > 0) {
|
||||
await rememberMeCheckbox.check();
|
||||
}
|
||||
|
||||
await loginPage.loginButton.click();
|
||||
await TestHelper.waitForUrl(page, /.*dashboard/);
|
||||
});
|
||||
|
||||
await test.step('验证Token持久化', async () => {
|
||||
const token = await TestHelper.getLocalStorage(page, 'token');
|
||||
expect(token).toBeTruthy();
|
||||
|
||||
const rememberMe = await TestHelper.getLocalStorage(page, 'remember_me');
|
||||
expect(rememberMe).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
test('登录成功 - 自动填充上次登录用户名', async ({ page }) => {
|
||||
await test.step('首次登录', async () => {
|
||||
await loginPage.usernameInput.fill('testuser');
|
||||
await loginPage.passwordInput.fill('testpassword');
|
||||
await loginPage.loginButton.click();
|
||||
await TestHelper.waitForUrl(page, /.*dashboard/);
|
||||
});
|
||||
|
||||
await test.step('登出', async () => {
|
||||
await loginPage.logout();
|
||||
await TestHelper.waitForUrl(page, /.*login/);
|
||||
});
|
||||
|
||||
await test.step('验证自动填充上次登录用户名', async () => {
|
||||
const usernameInput = page.locator('input[placeholder*="用户名"]');
|
||||
const usernameValue = await usernameInput.inputValue();
|
||||
expect(usernameValue).toBe('testuser');
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - SQL注入攻击', async ({ page }) => {
|
||||
await test.step('尝试SQL注入攻击', async () => {
|
||||
const sqlInjection = "' OR '1'='1";
|
||||
await loginPage.usernameInput.fill(sqlInjection);
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
});
|
||||
|
||||
await test.step('验证登录失败', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const currentUrl = page.url();
|
||||
expect(currentUrl).toContain('/login');
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - XSS攻击', async ({ page }) => {
|
||||
await test.step('尝试XSS攻击', async () => {
|
||||
const xssAttack = '<script>alert("XSS")</script>';
|
||||
await loginPage.usernameInput.fill(xssAttack);
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
});
|
||||
|
||||
await test.step('验证XSS被过滤', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const currentUrl = page.url();
|
||||
expect(currentUrl).toContain('/login');
|
||||
|
||||
const usernameInput = page.locator('input[placeholder*="用户名"]');
|
||||
const usernameValue = await usernameInput.inputValue();
|
||||
expect(usernameValue).not.toContain('<script>');
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 暴力破解防护', async ({ page }) => {
|
||||
await test.step('快速连续登录失败', async () => {
|
||||
const loginAttempts = 10;
|
||||
for (let i = 0; i < loginAttempts; i++) {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill(`wrongpassword${i}`);
|
||||
await loginPage.loginButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await loginPage.usernameInput.fill('');
|
||||
await loginPage.passwordInput.fill('');
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证账户被临时锁定', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('登录尝试次数过多');
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 网络错误', async ({ page }) => {
|
||||
await test.step('模拟网络错误', async () => {
|
||||
await page.route('**/api/auth/login', route => route.abort('failed'));
|
||||
});
|
||||
|
||||
await test.step('尝试登录', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
});
|
||||
|
||||
await test.step('验证网络错误提示', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('网络连接失败');
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 服务器错误', async ({ page }) => {
|
||||
await test.step('模拟服务器错误', async () => {
|
||||
await page.route('**/api/auth/login', route => {
|
||||
route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ message: 'Internal Server Error' })
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('尝试登录', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
});
|
||||
|
||||
await test.step('验证服务器错误提示', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('服务器错误');
|
||||
});
|
||||
});
|
||||
|
||||
test('登录成功 - 验证重定向保护', async ({ page }) => {
|
||||
await test.step('访问受保护页面', async () => {
|
||||
await page.goto('/users');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('验证重定向到登录页面', async () => {
|
||||
await TestHelper.waitForUrl(page, /.*login/);
|
||||
await expect(page).toHaveURL(/.*login/);
|
||||
});
|
||||
|
||||
await test.step('登录后验证重定向回原页面', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
|
||||
await TestHelper.waitForUrl(page, /.*users/);
|
||||
await expect(page).toHaveURL(/.*users/);
|
||||
});
|
||||
});
|
||||
|
||||
test('登录成功 - 验证会话管理', async ({ page, context }) => {
|
||||
await test.step('正常登录', async () => {
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
await TestHelper.waitForUrl(page, /.*dashboard/);
|
||||
});
|
||||
|
||||
await test.step('验证Session Cookie存在', async () => {
|
||||
const cookies = await context.cookies();
|
||||
const sessionCookie = cookies.find(c => c.name === 'SESSION' || c.name === 'JSESSIONID');
|
||||
expect(sessionCookie).toBeDefined();
|
||||
});
|
||||
|
||||
await test.step('验证Token存储在localStorage', async () => {
|
||||
const token = await TestHelper.getLocalStorage(page, 'token');
|
||||
expect(token).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('登录失败 - 验证CSRF保护', async ({ page }) => {
|
||||
await test.step('检查CSRF Token', async () => {
|
||||
const csrfToken = page.locator('input[name="csrf_token"]');
|
||||
const hasCsrfToken = await csrfToken.count() > 0;
|
||||
|
||||
if (hasCsrfToken) {
|
||||
const csrfValue = await csrfToken.inputValue();
|
||||
expect(csrfValue).toBeTruthy();
|
||||
expect(csrfValue.length).toBeGreaterThan(10);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,410 @@
|
||||
import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
class CustomReporter implements Reporter {
|
||||
private results: Map<string, TestCase[]> = new Map();
|
||||
private suiteResults: Map<string, Suite> = new Map();
|
||||
private startTime: number = Date.now();
|
||||
private testResults: TestResult[] = [];
|
||||
|
||||
onBegin(config: FullConfig) {
|
||||
console.log(`🚀 开始测试执行: ${config.projects.map(p => p.name).join(', ')}`);
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
onTestBegin(test: TestCase, result: TestResult) {
|
||||
console.log(`📝 开始测试: ${test.title}`);
|
||||
}
|
||||
|
||||
onTestEnd(test: TestCase, result: TestResult) {
|
||||
console.log(`✅ 测试完成: ${test.title} - ${result.status}`);
|
||||
this.testResults.push(result);
|
||||
}
|
||||
|
||||
onEnd(result: FullResult) {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - this.startTime;
|
||||
|
||||
console.log(`🎉 测试执行完成`);
|
||||
console.log(`⏱️ 总耗时: ${this.formatDuration(duration)}`);
|
||||
|
||||
const stats = this.calculateStats(result);
|
||||
this.generateConsoleReport(stats);
|
||||
this.generateHtmlReport(result, stats);
|
||||
this.generateJsonReport(result, stats);
|
||||
}
|
||||
|
||||
private calculateStats(result: FullResult): TestStats {
|
||||
const suites = result.suites || [];
|
||||
const allTests = suites.flatMap(suite =>
|
||||
suite.specs.flatMap(spec => spec.tests)
|
||||
);
|
||||
|
||||
const passed = allTests.filter(t => t.status === 'passed');
|
||||
const failed = allTests.filter(t => t.status === 'failed');
|
||||
const skipped = allTests.filter(t => t.status === 'skipped');
|
||||
const flaky = allTests.filter(t => t.status === 'passed' && t.retry >= 1);
|
||||
|
||||
const totalDuration = allTests.reduce((sum, t) => sum + (t.duration || 0), 0);
|
||||
const avgDuration = totalDuration / allTests.length;
|
||||
|
||||
const passRate = (passed.length / allTests.length) * 100;
|
||||
const failRate = (failed.length / allTests.length) * 100;
|
||||
const skipRate = (skipped.length / allTests.length) * 100;
|
||||
const flakyRate = (flaky.length / allTests.length) * 100;
|
||||
|
||||
return {
|
||||
total: allTests.length,
|
||||
passed: passed.length,
|
||||
failed: failed.length,
|
||||
skipped: skipped.length,
|
||||
flaky: flaky.length,
|
||||
passRate,
|
||||
failRate,
|
||||
skipRate,
|
||||
flakyRate,
|
||||
totalDuration,
|
||||
avgDuration,
|
||||
slowestTests: allTests
|
||||
.filter(t => t.duration)
|
||||
.sort((a, b) => (b.duration || 0) - (a.duration || 0))
|
||||
.slice(0, 10),
|
||||
failedTests: failed,
|
||||
};
|
||||
}
|
||||
|
||||
private generateConsoleReport(stats: TestStats) {
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('📊 测试统计报告');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('');
|
||||
console.log(`📈 总测试数: ${stats.total}`);
|
||||
console.log(`✅ 通过: ${stats.passed} (${stats.passRate.toFixed(2)}%)`);
|
||||
console.log(`❌ 失败: ${stats.failed} (${stats.failRate.toFixed(2)}%)`);
|
||||
console.log(`⏭️ 跳过: ${stats.skipped} (${stats.skipRate.toFixed(2)}%)`);
|
||||
console.log(`🔄 不稳定: ${stats.flaky} (${stats.flakyRate.toFixed(2)}%)`);
|
||||
console.log('');
|
||||
console.log(`⏱️ 总耗时: ${this.formatDuration(stats.totalDuration)}`);
|
||||
console.log(`⏱️ 平均耗时: ${this.formatDuration(stats.avgDuration)}`);
|
||||
console.log('');
|
||||
console.log('🐌 最慢的10个测试:');
|
||||
stats.slowestTests.forEach((test, index) => {
|
||||
console.log(` ${index + 1}. ${test.title} - ${this.formatDuration(test.duration || 0)}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
if (stats.failedTests.length > 0) {
|
||||
console.log('❌ 失败的测试:');
|
||||
stats.failedTests.forEach((test, index) => {
|
||||
console.log(` ${index + 1}. ${test.title}`);
|
||||
console.log(` 位置: ${test.location.file}:${test.location.line}`);
|
||||
console.log(` 错误: ${test.error?.message}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
private generateHtmlReport(result: FullResult, stats: TestStats) {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>测试报告 - Novalon管理系统</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
padding: 30px;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.stat-card h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.stat-card .value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
.stat-card .label {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.stat-card.passed {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
}
|
||||
.stat-card.failed {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #f44336 100%);
|
||||
}
|
||||
.stat-card.flaky {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #f093fb 100%);
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.section h2 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.test-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.test-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 4px solid #ddd;
|
||||
background: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.test-item.passed {
|
||||
border-left-color: #38ef7d;
|
||||
background: #f0fff4;
|
||||
}
|
||||
.test-item.failed {
|
||||
border-left-color: #ef4444;
|
||||
background: #fff5f5;
|
||||
}
|
||||
.test-item.skipped {
|
||||
border-left-color: #f59e0b;
|
||||
background: #fef9c3;
|
||||
}
|
||||
.test-item.flaky {
|
||||
border-left-color: #f093fb;
|
||||
background: #fef3c7;
|
||||
}
|
||||
.test-item .test-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
.test-item .test-duration {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
.test-item .test-error {
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
padding: 10px;
|
||||
background: #fee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 20px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🧪 Novalon管理系统测试报告</h1>
|
||||
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card passed">
|
||||
<h3>通过测试</h3>
|
||||
<div class="value">${stats.passed}</div>
|
||||
<div class="label">${stats.passRate.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div class="stat-card failed">
|
||||
<h3>失败测试</h3>
|
||||
<div class="value">${stats.failed}</div>
|
||||
<div class="label">${stats.failRate.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div class="stat-card flaky">
|
||||
<h3>不稳定测试</h3>
|
||||
<div class="value">${stats.flaky}</div>
|
||||
<div class="label">${stats.flakyRate.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>总测试数</h3>
|
||||
<div class="value">${stats.total}</div>
|
||||
<div class="label">100%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-fill" style="width: ${stats.passRate}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📈 测试统计</h2>
|
||||
<ul class="test-list">
|
||||
<li class="test-item">
|
||||
<div class="test-name">总耗时</div>
|
||||
<div class="test-duration">${this.formatDuration(stats.totalDuration)}</div>
|
||||
</li>
|
||||
<li class="test-item">
|
||||
<div class="test-name">平均耗时</div>
|
||||
<div class="test-duration">${this.formatDuration(stats.avgDuration)}</div>
|
||||
</li>
|
||||
<li class="test-item">
|
||||
<div class="test-name">跳过测试</div>
|
||||
<div class="test-duration">${stats.skipped} (${stats.skipRate.toFixed(2)}%)</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
${stats.failedTests.length > 0 ? `
|
||||
<div class="section">
|
||||
<h2>❌ 失败测试详情</h2>
|
||||
<ul class="test-list">
|
||||
${stats.failedTests.map(test => `
|
||||
<li class="test-item failed">
|
||||
<div class="test-name">${test.title}</div>
|
||||
<div class="test-duration">${this.formatDuration(test.duration || 0)}</div>
|
||||
<div class="test-error">
|
||||
<strong>错误:</strong> ${test.error?.message || '未知错误'}
|
||||
</div>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="section">
|
||||
<h2>🐌 最慢的10个测试</h2>
|
||||
<ul class="test-list">
|
||||
${stats.slowestTests.map((test, index) => `
|
||||
<li class="test-item ${test.status}">
|
||||
<div class="test-name">${index + 1}. ${test.title}</div>
|
||||
<div class="test-duration">${this.formatDuration(test.duration || 0)}</div>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>🧪 Novalon管理系统 - 自动化测试报告</p>
|
||||
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.html');
|
||||
fs.writeFileSync(reportPath, html, 'utf-8');
|
||||
console.log(`📄 HTML报告已生成: ${reportPath}`);
|
||||
}
|
||||
|
||||
private generateJsonReport(result: FullResult, stats: TestStats) {
|
||||
const report = {
|
||||
summary: {
|
||||
timestamp: new Date().toISOString(),
|
||||
total: stats.total,
|
||||
passed: stats.passed,
|
||||
failed: stats.failed,
|
||||
skipped: stats.skipped,
|
||||
flaky: stats.flaky,
|
||||
passRate: stats.passRate,
|
||||
failRate: stats.failRate,
|
||||
skipRate: stats.skipRate,
|
||||
flakyRate: stats.flakyRate,
|
||||
totalDuration: stats.totalDuration,
|
||||
avgDuration: stats.avgDuration,
|
||||
},
|
||||
failedTests: stats.failedTests.map(test => ({
|
||||
title: test.title,
|
||||
location: test.location,
|
||||
error: test.error?.message,
|
||||
duration: test.duration,
|
||||
})),
|
||||
slowestTests: stats.slowestTests.map(test => ({
|
||||
title: test.title,
|
||||
duration: test.duration,
|
||||
})),
|
||||
};
|
||||
|
||||
const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.json');
|
||||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
|
||||
console.log(`📄 JSON报告已生成: ${reportPath}`);
|
||||
}
|
||||
|
||||
private formatDuration(ms: number): string {
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
} else if (ms < 60000) {
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
} else {
|
||||
return `${(ms / 60000).toFixed(1)}m`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TestStats {
|
||||
total: number;
|
||||
passed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
flaky: number;
|
||||
passRate: number;
|
||||
failRate: number;
|
||||
skipRate: number;
|
||||
flakyRate: number;
|
||||
totalDuration: number;
|
||||
avgDuration: number;
|
||||
slowestTests: TestCase[];
|
||||
}
|
||||
|
||||
export default CustomReporter;
|
||||
@@ -0,0 +1,323 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
import { RoleManagementPage } from './pages/RoleManagementPage';
|
||||
import { TestHelper } from './utils/testHelper';
|
||||
|
||||
test.describe('边缘场景测试', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
let userManagementPage: UserManagementPage;
|
||||
let roleManagementPage: RoleManagementPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
userManagementPage = new UserManagementPage(page);
|
||||
roleManagementPage = new RoleManagementPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await TestHelper.clearAllStorage(page);
|
||||
});
|
||||
|
||||
test.describe('边界值测试', () => {
|
||||
test('用户名边界值 - 最小长度', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建最小长度用户名的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const minUsername = 'ab';
|
||||
await userManagementPage.fillUserForm({
|
||||
username: minUsername,
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证用户创建成功', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户名边界值 - 最大长度', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建最大长度用户名的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const maxUsername = 'a'.repeat(50);
|
||||
await userManagementPage.fillUserForm({
|
||||
username: maxUsername,
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证用户创建成功', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('密码边界值 - 最小长度', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建最小长度密码的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const minPassword = 'a'.repeat(6);
|
||||
await userManagementPage.fillUserForm({
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: minPassword
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证用户创建成功', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('密码边界值 - 最大长度', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建最大长度密码的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const maxPassword = 'a'.repeat(20);
|
||||
await userManagementPage.fillUserForm({
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: maxPassword
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证用户创建成功', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('空值和null值测试', () => {
|
||||
test('用户创建 - 用户名为空', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建用户名为空的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUserForm({
|
||||
username: '',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证用户名必填验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('用户名不能为空');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户创建 - 密码为空', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建密码为空的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUserForm({
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: ''
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证密码必填验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('密码不能为空');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户创建 - 邮箱为空', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建邮箱为空的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUserForm({
|
||||
username: 'testuser',
|
||||
email: '',
|
||||
password: 'password123'
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证邮箱必填验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('邮箱不能为空');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('特殊字符和格式测试', () => {
|
||||
test('用户名 - 包含中文字符', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建包含中文的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUserForm({
|
||||
username: '测试用户',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证中文用户名处理', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户名 - 包含emoji表情', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建包含emoji的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUserForm({
|
||||
username: 'test😀user',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证emoji用户名处理', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('密码 - 包含特殊字符', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建包含特殊字符密码的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUserForm({
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'P@ssw0rd!#$'
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证特殊字符密码处理', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('并发和竞态条件测试', () => {
|
||||
test('快速连续操作', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('快速连续点击创建按钮', async () => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await page.click('.create-button');
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证重复点击处理', async () => {
|
||||
const dialogs = await page.locator('.el-dialog').count();
|
||||
expect(dialogs).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('国际化场景测试', () => {
|
||||
test('中文界面操作', async ({ page }) => {
|
||||
await test.step('验证中文界面显示', async () => {
|
||||
const dashboardTitle = await page.textContent('h1');
|
||||
expect(dashboardTitle).toContain('仪表盘');
|
||||
});
|
||||
|
||||
await test.step('验证中文按钮文本', async () => {
|
||||
const createButton = await page.textContent('.create-button');
|
||||
expect(createButton).toContain('创建');
|
||||
});
|
||||
});
|
||||
|
||||
test('中英文混合输入', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建中英文混合用户名的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUserForm({
|
||||
username: 'test测试user',
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证中英文混合处理', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,534 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
import { RoleManagementPage } from './pages/RoleManagementPage';
|
||||
import { TestHelper } from './utils/testHelper';
|
||||
|
||||
test.describe('边缘场景测试', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
let userManagementPage: UserManagementPage;
|
||||
let roleManagementPage: RoleManagementPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
userManagementPage = new UserManagementPage(page);
|
||||
roleManagementPage = new RoleManagementPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await TestHelper.clearAllStorage(page);
|
||||
});
|
||||
|
||||
test.describe('边界值测试', () => {
|
||||
test('用户名边界值 - 最小长度', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建最小长度用户名的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const minUsername = 'ab';
|
||||
await userManagementPage.fillUserForm({
|
||||
username: minUsername,
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
});
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证用户创建成功', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户名边界值 - 最大长度', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建最大长度用户名的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const maxUsername = 'a'.repeat(50);
|
||||
await userManagementPage.fillUsername(maxUsername);
|
||||
await userManagementPage.fillPassword('password123');
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证用户创建成功', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户名边界值 - 超过最大长度', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建超过最大长度用户名的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const exceedUsername = 'a'.repeat(51);
|
||||
await userManagementPage.fillUsername(exceedUsername);
|
||||
await userManagementPage.fillPassword('password123');
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证用户名长度验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('用户名长度不能超过50个字符');
|
||||
});
|
||||
});
|
||||
|
||||
test('密码边界值 - 最小长度', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建最小长度密码的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('testuser');
|
||||
const minPassword = 'a'.repeat(6);
|
||||
await userManagementPage.fillPassword(minPassword);
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证用户创建成功', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('密码边界值 - 最大长度', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建最大长度密码的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('testuser');
|
||||
const maxPassword = 'a'.repeat(20);
|
||||
await userManagementPage.fillPassword(maxPassword);
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证用户创建成功', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('密码边界值 - 低于最小长度', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建低于最小长度密码的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('testuser');
|
||||
const shortPassword = 'a'.repeat(5);
|
||||
await userManagementPage.fillPassword(shortPassword);
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证密码长度验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('密码长度不能少于6个字符');
|
||||
});
|
||||
});
|
||||
|
||||
test('邮箱边界值 - 无效格式', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建无效邮箱格式的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('testuser');
|
||||
await userManagementPage.fillPassword('password123');
|
||||
await userManagementPage.fillEmail('invalid-email');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证邮箱格式验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('邮箱格式不正确');
|
||||
});
|
||||
});
|
||||
|
||||
test('角色名边界值 - 特殊字符', async ({ page }) => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建包含特殊字符的角色', async () => {
|
||||
await roleManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const specialCharRole = '角色@#$%';
|
||||
await roleManagementPage.fillRoleName(specialCharRole);
|
||||
await roleManagementPage.fillRoleKey('ROLE_SPECIAL');
|
||||
await roleManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证特殊字符处理', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('空值和null值测试', () => {
|
||||
test('用户创建 - 用户名为空', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建用户名为空的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('');
|
||||
await userManagementPage.fillPassword('password123');
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证用户名必填验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('用户名不能为空');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户创建 - 密码为空', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建密码为空的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('testuser');
|
||||
await userManagementPage.fillPassword('');
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证密码必填验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('密码不能为空');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户创建 - 邮箱为空', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建邮箱为空的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('testuser');
|
||||
await userManagementPage.fillPassword('password123');
|
||||
await userManagementPage.fillEmail('');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证邮箱必填验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('邮箱不能为空');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户创建 - 角色为空', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建角色为空的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('testuser');
|
||||
await userManagementPage.fillPassword('password123');
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证角色必填验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('角色不能为空');
|
||||
});
|
||||
});
|
||||
|
||||
test('角色创建 - 角色名为空', async ({ page }) => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建角色名为空的角色', async () => {
|
||||
await roleManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await roleManagementPage.fillRoleName('');
|
||||
await roleManagementPage.fillRoleKey('ROLE_EMPTY');
|
||||
await roleManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证角色名必填验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('角色名不能为空');
|
||||
});
|
||||
});
|
||||
|
||||
test('角色创建 - 角色键为空', async ({ page }) => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建角色键为空的角色', async () => {
|
||||
await roleManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await roleManagementPage.fillRoleName('测试角色');
|
||||
await roleManagementPage.fillRoleKey('');
|
||||
await roleManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证角色键必填验证', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('角色键不能为空');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('特殊字符和格式测试', () => {
|
||||
test('用户名 - 包含中文字符', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建包含中文的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('测试用户');
|
||||
await userManagementPage.fillPassword('password123');
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证中文用户名处理', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户名 - 包含emoji表情', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建包含emoji的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('test😀user');
|
||||
await userManagementPage.fillPassword('password123');
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证emoji用户名处理', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('密码 - 包含特殊字符', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建包含特殊字符密码的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('testuser');
|
||||
await userManagementPage.fillPassword('P@ssw0rd!#$');
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证特殊字符密码处理', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
|
||||
test('邮箱 - 包含特殊字符', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建包含特殊字符邮箱的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('testuser');
|
||||
await userManagementPage.fillPassword('password123');
|
||||
await userManagementPage.fillEmail('test.user+tag@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证特殊字符邮箱处理', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('并发和竞态条件测试', () => {
|
||||
test('并发创建相同用户名', async ({ page, context }) => {
|
||||
const page1 = page;
|
||||
const page2 = await context.newPage();
|
||||
|
||||
await test.step('在两个页面同时创建相同用户名的用户', async () => {
|
||||
await page1.goto('/users');
|
||||
await page2.goto('/users');
|
||||
|
||||
await TestHelper.waitForPageLoad(page1);
|
||||
await TestHelper.waitForPageLoad(page2);
|
||||
|
||||
await page1.click('.create-button');
|
||||
await page2.click('.create-button');
|
||||
|
||||
await TestHelper.waitForElementVisible(page1, '.el-dialog');
|
||||
await TestHelper.waitForElementVisible(page2, '.el-dialog');
|
||||
|
||||
await page1.fill('input[name="username"]', 'duplicateuser');
|
||||
await page2.fill('input[name="username"]', 'duplicateuser');
|
||||
|
||||
await page1.fill('input[name="password"]', 'password123');
|
||||
await page2.fill('input[name="password"]', 'password123');
|
||||
|
||||
await page1.fill('input[name="email"]', 'test1@example.com');
|
||||
await page2.fill('input[name="email"]', 'test2@example.com');
|
||||
|
||||
await page1.click('.el-dialog__footer button[type="submit"]');
|
||||
await page2.click('.el-dialog__footer button[type="submit"]');
|
||||
});
|
||||
|
||||
await test.step('验证并发冲突处理', async () => {
|
||||
await TestHelper.waitForPageLoad(page1);
|
||||
await TestHelper.waitForPageLoad(page2);
|
||||
|
||||
const errorMessage1 = await TestHelper.getElementText(page1, '.el-message__content');
|
||||
const errorMessage2 = await TestHelper.getElementText(page2, '.el-message__content');
|
||||
|
||||
expect(errorMessage1 || errorMessage2).toContain('用户名已存在');
|
||||
});
|
||||
});
|
||||
|
||||
test('快速连续操作', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('快速连续点击创建按钮', async () => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await page.click('.create-button');
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证重复点击处理', async () => {
|
||||
const dialogs = await page.locator('.el-dialog').count();
|
||||
expect(dialogs).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('国际化场景测试', () => {
|
||||
test('中文界面操作', async ({ page }) => {
|
||||
await test.step('验证中文界面显示', async () => {
|
||||
const dashboardTitle = await page.textContent('h1');
|
||||
expect(dashboardTitle).toContain('仪表盘');
|
||||
});
|
||||
|
||||
await test.step('验证中文按钮文本', async () => {
|
||||
const createButton = await page.textContent('.create-button');
|
||||
expect(createButton).toContain('创建');
|
||||
});
|
||||
|
||||
await test.step('验证中文表单标签', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
const usernameLabel = await page.textContent('label[for="username"]');
|
||||
expect(usernameLabel).toContain('用户名');
|
||||
});
|
||||
});
|
||||
|
||||
test('中英文混合输入', async ({ page }) => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
await test.step('创建中英文混合用户名的用户', async () => {
|
||||
await userManagementPage.clickCreateButton();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
await userManagementPage.fillUsername('test测试user');
|
||||
await userManagementPage.fillPassword('password123');
|
||||
await userManagementPage.fillEmail('test@example.com');
|
||||
await userManagementPage.selectRole('管理员');
|
||||
await userManagementPage.clickSaveButton();
|
||||
});
|
||||
|
||||
await test.step('验证中英文混合处理', async () => {
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(successMessage).toContain('创建成功');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -64,11 +64,11 @@ export class RoleManagementPage {
|
||||
}
|
||||
|
||||
async confirmDelete() {
|
||||
await this.page.getByRole('button', { name: '确定' }).or(page.locator('.confirm-dialog .confirm-button')).click();
|
||||
await this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.confirm-dialog .confirm-button')).click();
|
||||
}
|
||||
|
||||
async openPermissionDialog(rowNumber: number) {
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '权限' }).or(page.locator(`tbody tr:nth-child(${rowNumber}) .permission-button`)).click();
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '权限' }).or(this.page.locator(`tbody tr:nth-child(${rowNumber}) .permission-button`)).click();
|
||||
}
|
||||
|
||||
async selectPermission(permissionValue: string) {
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
import { RoleManagementPage } from './pages/RoleManagementPage';
|
||||
import { TestHelper } from './utils/testHelper';
|
||||
|
||||
test.describe('测试并行化验证', () => {
|
||||
test('并行执行多个独立测试', async ({ page, context }) => {
|
||||
const page1 = page;
|
||||
const page2 = await context.newPage();
|
||||
const page3 = await context.newPage();
|
||||
|
||||
const loginPage1 = new LoginPage(page1);
|
||||
const loginPage2 = new LoginPage(page2);
|
||||
const loginPage3 = new LoginPage(page3);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('并行登录三个页面', async () => {
|
||||
await Promise.all([
|
||||
loginPage1.goto(),
|
||||
loginPage2.goto(),
|
||||
loginPage3.goto()
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
loginPage1.login('admin', 'admin123'),
|
||||
loginPage2.login('admin', 'admin123'),
|
||||
loginPage3.login('admin', 'admin123')
|
||||
]);
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const parallelTime = endTime - startTime;
|
||||
|
||||
console.log(`并行登录时间: ${parallelTime}ms`);
|
||||
expect(parallelTime).toBeLessThan(5000);
|
||||
|
||||
await test.step('验证所有页面登录成功', async () => {
|
||||
const url1 = page1.url();
|
||||
const url2 = page2.url();
|
||||
const url3 = page3.url();
|
||||
|
||||
expect(url1).toContain('/dashboard');
|
||||
expect(url2).toContain('/dashboard');
|
||||
expect(url3).toContain('/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
test('并行加载不同模块', async ({ page, context }) => {
|
||||
const page1 = page;
|
||||
const page2 = await context.newPage();
|
||||
const page3 = await context.newPage();
|
||||
|
||||
const loginPage1 = new LoginPage(page1);
|
||||
const loginPage2 = new LoginPage(page2);
|
||||
const loginPage3 = new LoginPage(page3);
|
||||
|
||||
await loginPage1.goto();
|
||||
await loginPage1.login('admin', 'admin123');
|
||||
await loginPage2.goto();
|
||||
await loginPage2.login('admin', 'admin123');
|
||||
await loginPage3.goto();
|
||||
await loginPage3.login('admin', 'admin123');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('并行加载用户、角色、设置模块', async () => {
|
||||
await Promise.all([
|
||||
page1.goto('/users'),
|
||||
page2.goto('/roles'),
|
||||
page3.goto('/settings')
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
page1.waitForSelector('[data-testid="user-table"]'),
|
||||
page2.waitForSelector('[data-testid="role-table"]'),
|
||||
page3.waitForSelector('[data-testid="settings-form"]')
|
||||
]);
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const parallelLoadTime = endTime - startTime;
|
||||
|
||||
console.log(`并行加载时间: ${parallelLoadTime}ms`);
|
||||
expect(parallelLoadTime).toBeLessThan(3000);
|
||||
});
|
||||
|
||||
test('并发API请求性能', async ({ page, request }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('并发发送多个API请求', async () => {
|
||||
const token = await TestHelper.getAuthToken(page);
|
||||
|
||||
const promises = [
|
||||
request.get('http://localhost:8084/api/users', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
}),
|
||||
request.get('http://localhost:8084/api/roles', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
}),
|
||||
request.get('http://localhost:8084/api/permissions', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
}),
|
||||
request.get('http://localhost:8084/api/departments', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
expect(results[0].status()).toBe(200);
|
||||
expect(results[1].status()).toBe(200);
|
||||
expect(results[2].status()).toBe(200);
|
||||
expect(results[3].status()).toBe(200);
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const concurrentApiTime = endTime - startTime;
|
||||
|
||||
console.log(`并发API请求时间: ${concurrentApiTime}ms`);
|
||||
expect(concurrentApiTime).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('测试隔离验证', async ({ page, context }) => {
|
||||
const page1 = page;
|
||||
const page2 = await context.newPage();
|
||||
|
||||
const loginPage1 = new LoginPage(page1);
|
||||
const loginPage2 = new LoginPage(page2);
|
||||
|
||||
await loginPage1.goto();
|
||||
await loginPage1.login('admin', 'admin123');
|
||||
await loginPage2.goto();
|
||||
await loginPage2.login('testuser', 'test123');
|
||||
|
||||
await test.step('验证页面状态隔离', async () => {
|
||||
const url1 = page1.url();
|
||||
const url2 = page2.url();
|
||||
|
||||
expect(url1).toContain('/dashboard');
|
||||
expect(url2).toContain('/dashboard');
|
||||
|
||||
const storage1 = await page1.evaluate(() => {
|
||||
return localStorage.getItem('user');
|
||||
});
|
||||
|
||||
const storage2 = await page2.evaluate(() => {
|
||||
return localStorage.getItem('user');
|
||||
});
|
||||
|
||||
expect(storage1).not.toBe(storage2);
|
||||
});
|
||||
|
||||
await test.step('验证页面操作隔离', async () => {
|
||||
await page1.goto('/users');
|
||||
await page2.goto('/roles');
|
||||
|
||||
await page1.waitForSelector('[data-testid="user-table"]');
|
||||
await page2.waitForSelector('[data-testid="role-table"]');
|
||||
|
||||
const url1 = page1.url();
|
||||
const url2 = page2.url();
|
||||
|
||||
expect(url1).toContain('/users');
|
||||
expect(url2).toContain('/roles');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('测试分组策略', () => {
|
||||
test('按模块分组执行', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
|
||||
const userModuleTests = [
|
||||
async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
},
|
||||
async () => {
|
||||
await page.goto('/users/create');
|
||||
await page.waitForSelector('[data-testid="user-form"]');
|
||||
}
|
||||
];
|
||||
|
||||
const roleModuleTests = [
|
||||
async () => {
|
||||
await page.goto('/roles');
|
||||
await page.waitForSelector('[data-testid="role-table"]');
|
||||
},
|
||||
async () => {
|
||||
await page.goto('/roles/create');
|
||||
await page.waitForSelector('[data-testid="role-form"]');
|
||||
}
|
||||
];
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('按模块顺序执行测试', async () => {
|
||||
for (const test of userModuleTests) {
|
||||
await test();
|
||||
}
|
||||
|
||||
for (const test of roleModuleTests) {
|
||||
await test();
|
||||
}
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const sequentialTime = endTime - startTime;
|
||||
|
||||
console.log(`顺序执行时间: ${sequentialTime}ms`);
|
||||
});
|
||||
|
||||
test('按优先级分组执行', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
|
||||
const highPriorityTests = [
|
||||
async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
}
|
||||
];
|
||||
|
||||
const lowPriorityTests = [
|
||||
async () => {
|
||||
await page.goto('/settings');
|
||||
await page.waitForSelector('[data-testid="settings-form"]');
|
||||
}
|
||||
];
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('按优先级执行测试', async () => {
|
||||
for (const test of highPriorityTests) {
|
||||
await test();
|
||||
}
|
||||
|
||||
for (const test of lowPriorityTests) {
|
||||
await test();
|
||||
}
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const priorityTime = endTime - startTime;
|
||||
|
||||
console.log(`优先级执行时间: ${priorityTime}ms`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('测试依赖优化', () => {
|
||||
test('减少测试间依赖', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('执行独立测试', async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
|
||||
await page.goto('/roles');
|
||||
await page.waitForSelector('[data-testid="role-table"]');
|
||||
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const independentTime = endTime - startTime;
|
||||
|
||||
console.log(`独立测试执行时间: ${independentTime}ms`);
|
||||
expect(independentTime).toBeLessThan(5000);
|
||||
});
|
||||
|
||||
test('优化测试清理逻辑', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('快速清理测试状态', async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
|
||||
await TestHelper.clearAllStorage(page);
|
||||
|
||||
await page.goto('/roles');
|
||||
await page.waitForSelector('[data-testid="role-table"]');
|
||||
|
||||
await TestHelper.clearAllStorage(page);
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const cleanupTime = endTime - startTime;
|
||||
|
||||
console.log(`清理操作时间: ${cleanupTime}ms`);
|
||||
expect(cleanupTime).toBeLessThan(3000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,488 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
import { RoleManagementPage } from './pages/RoleManagementPage';
|
||||
|
||||
test.describe('性能测试基准', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
let userManagementPage: UserManagementPage;
|
||||
let roleManagementPage: RoleManagementPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
userManagementPage = new UserManagementPage(page);
|
||||
roleManagementPage = new RoleManagementPage(page);
|
||||
});
|
||||
|
||||
test('登录页面加载性能', async ({ page }) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await loginPage.goto();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const endTime = Date.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
console.log(`登录页面加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(3000);
|
||||
});
|
||||
|
||||
test('登录操作性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
await loginPage.loginButton.click();
|
||||
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const endTime = Date.now();
|
||||
const loginTime = endTime - startTime;
|
||||
|
||||
console.log(`登录操作时间: ${loginTime}ms`);
|
||||
expect(loginTime).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('Dashboard页面加载性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/dashboard');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const endTime = Date.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
console.log(`Dashboard页面加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('用户管理页面加载性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const endTime = Date.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
console.log(`用户管理页面加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('角色管理页面加载性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const endTime = Date.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
console.log(`角色管理页面加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('用户列表加载性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const table = page.locator('.el-table').first();
|
||||
await expect(table).toBeVisible();
|
||||
|
||||
const endTime = Date.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
console.log(`用户列表加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(1500);
|
||||
});
|
||||
|
||||
test('角色列表加载性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const table = page.locator('.el-table').first();
|
||||
await expect(table).toBeVisible();
|
||||
|
||||
const endTime = Date.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
console.log(`角色列表加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(1500);
|
||||
});
|
||||
|
||||
test('创建用户对话框打开性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await userManagementPage.clickCreateUser();
|
||||
await expect(page.locator('.el-dialog')).toBeVisible();
|
||||
|
||||
const endTime = Date.now();
|
||||
const openTime = endTime - startTime;
|
||||
|
||||
console.log(`创建用户对话框打开时间: ${openTime}ms`);
|
||||
expect(openTime).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test('创建角色对话框打开性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await roleManagementPage.clickCreateRole();
|
||||
await expect(page.locator('.el-dialog')).toBeVisible();
|
||||
|
||||
const endTime = Date.now();
|
||||
const openTime = endTime - startTime;
|
||||
|
||||
console.log(`创建角色对话框打开时间: ${openTime}ms`);
|
||||
expect(openTime).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test('用户搜索性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await userManagementPage.search('admin');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const endTime = Date.now();
|
||||
const searchTime = endTime - startTime;
|
||||
|
||||
console.log(`用户搜索时间: ${searchTime}ms`);
|
||||
expect(searchTime).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test('角色搜索性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const searchInput = page.locator('input[placeholder*="搜索"]').or(page.locator('.search-input'));
|
||||
if (await searchInput.count() > 0) {
|
||||
await searchInput.fill('admin');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const searchTime = endTime - startTime;
|
||||
|
||||
console.log(`角色搜索时间: ${searchTime}ms`);
|
||||
expect(searchTime).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test('用户表单提交性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await userManagementPage.clickCreateUser();
|
||||
await expect(page.locator('.el-dialog')).toBeVisible();
|
||||
|
||||
const userData = {
|
||||
username: `testuser_${Date.now()}`,
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com',
|
||||
phone: '13800138000',
|
||||
password: 'Test123!@#',
|
||||
confirmPassword: 'Test123!@#',
|
||||
};
|
||||
|
||||
await userManagementPage.fillUserForm(userData);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await userManagementPage.submitForm();
|
||||
await expect(page.locator('.el-message--success')).toBeVisible();
|
||||
|
||||
const endTime = Date.now();
|
||||
const submitTime = endTime - startTime;
|
||||
|
||||
console.log(`用户表单提交时间: ${submitTime}ms`);
|
||||
expect(submitTime).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('角色表单提交性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await roleManagementPage.clickCreateRole();
|
||||
await expect(page.locator('.el-dialog')).toBeVisible();
|
||||
|
||||
const roleData = {
|
||||
roleName: `测试角色_${Date.now()}`,
|
||||
roleKey: `test_role_${Date.now()}`,
|
||||
roleSort: '1',
|
||||
status: '1',
|
||||
remark: '测试角色',
|
||||
};
|
||||
|
||||
await roleManagementPage.fillRoleForm(roleData);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await roleManagementPage.submitForm();
|
||||
await expect(page.locator('.el-message--success')).toBeVisible();
|
||||
|
||||
const endTime = Date.now();
|
||||
const submitTime = endTime - startTime;
|
||||
|
||||
console.log(`角色表单提交时间: ${submitTime}ms`);
|
||||
expect(submitTime).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('页面切换性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const switchTimes = 5;
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < switchTimes; i++) {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const avgSwitchTime = (endTime - startTime) / switchTimes;
|
||||
|
||||
console.log(`平均页面切换时间: ${avgSwitchTime}ms`);
|
||||
expect(avgSwitchTime).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test('表格滚动性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const table = page.locator('.el-table').first();
|
||||
await expect(table).toBeVisible();
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await table.evaluate(el => {
|
||||
el.scrollTop = 1000;
|
||||
});
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const endTime = Date.now();
|
||||
const scrollTime = endTime - startTime;
|
||||
|
||||
console.log(`表格滚动时间: ${scrollTime}ms`);
|
||||
expect(scrollTime).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('内存使用性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const metrics = await page.evaluate(() => {
|
||||
if (window.performance && (window.performance as any).memory) {
|
||||
const perfMemory = (window.performance as any).memory;
|
||||
return {
|
||||
usedJSHeapSize: perfMemory.usedJSHeapSize,
|
||||
totalJSHeapSize: perfMemory.totalJSHeapSize,
|
||||
jsHeapSizeLimit: perfMemory.jsHeapSizeLimit,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (metrics) {
|
||||
console.log('内存使用情况:', metrics);
|
||||
|
||||
const memoryUsageRatio = metrics.usedJSHeapSize / metrics.jsHeapSizeLimit;
|
||||
expect(memoryUsageRatio).toBeLessThan(0.8);
|
||||
}
|
||||
});
|
||||
|
||||
test('网络请求性能', async ({ page }) => {
|
||||
const apiRequests: { url: string; duration: number }[] = [];
|
||||
|
||||
page.on('response', async (response) => {
|
||||
if (response.url().includes('/api/')) {
|
||||
const timing = (response as any).timing();
|
||||
const duration = timing.responseEnd - timing.requestStart;
|
||||
apiRequests.push({
|
||||
url: response.url(),
|
||||
duration,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
if (apiRequests.length > 0) {
|
||||
const avgDuration = apiRequests.reduce((sum, req) => sum + req.duration, 0) / apiRequests.length;
|
||||
const maxDuration = Math.max(...apiRequests.map(req => req.duration));
|
||||
|
||||
console.log(`API请求平均时间: ${avgDuration}ms`);
|
||||
console.log(`API请求最大时间: ${maxDuration}ms`);
|
||||
|
||||
expect(avgDuration).toBeLessThan(500);
|
||||
expect(maxDuration).toBeLessThan(2000);
|
||||
}
|
||||
});
|
||||
|
||||
test('并发操作性能', async ({ page, context }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const page1 = page;
|
||||
const page2 = await context.newPage();
|
||||
const page3 = await context.newPage();
|
||||
|
||||
await Promise.all([
|
||||
page1.goto('/users'),
|
||||
page2.goto('/roles'),
|
||||
page3.goto('/menus'),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
page1.waitForLoadState('networkidle'),
|
||||
page2.waitForLoadState('networkidle'),
|
||||
page3.waitForLoadState('networkidle'),
|
||||
]);
|
||||
|
||||
const endTime = Date.now();
|
||||
const concurrentLoadTime = endTime - startTime;
|
||||
|
||||
console.log(`并发页面加载时间: ${concurrentLoadTime}ms`);
|
||||
expect(concurrentLoadTime).toBeLessThan(5000);
|
||||
|
||||
await page2.close();
|
||||
await page3.close();
|
||||
});
|
||||
|
||||
test('长时间运行稳定性', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
const duration = 60000; // 1分钟
|
||||
|
||||
let operationCount = 0;
|
||||
const interval = setInterval(async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.goto('/roles');
|
||||
await page.waitForLoadState('networkidle');
|
||||
operationCount++;
|
||||
}, 5000);
|
||||
|
||||
await page.waitForTimeout(duration);
|
||||
clearInterval(interval);
|
||||
|
||||
const endTime = Date.now();
|
||||
const actualDuration = endTime - startTime;
|
||||
|
||||
console.log(`长时间运行操作次数: ${operationCount}`);
|
||||
console.log(`长时间运行实际时间: ${actualDuration}ms`);
|
||||
|
||||
expect(operationCount).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
test('响应式布局性能', async ({ page }) => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const viewports = [
|
||||
{ width: 1920, height: 1080 },
|
||||
{ width: 1366, height: 768 },
|
||||
{ width: 768, height: 1024 },
|
||||
{ width: 375, height: 667 },
|
||||
];
|
||||
|
||||
for (const viewport of viewports) {
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.setViewportSize(viewport);
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const endTime = Date.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
console.log(`视口 ${viewport.width}x${viewport.height} 加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(3000);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,417 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
import { RoleManagementPage } from './pages/RoleManagementPage';
|
||||
import { TestHelper } from './utils/testHelper';
|
||||
|
||||
test.describe('性能优化测试', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
let userManagementPage: UserManagementPage;
|
||||
let roleManagementPage: RoleManagementPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
userManagementPage = new UserManagementPage(page);
|
||||
roleManagementPage = new RoleManagementPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await TestHelper.clearAllStorage(page);
|
||||
});
|
||||
|
||||
test.describe('等待策略优化测试', () => {
|
||||
test('登录页面 - 使用精确等待', async ({ page }) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('等待登录页面加载完成', async () => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('[data-testid="login-form"]', { state: 'visible' });
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
console.log(`登录页面加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(3000);
|
||||
});
|
||||
|
||||
test('用户列表 - 使用智能等待', async ({ page }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('等待用户列表加载完成', async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForSelector('[data-testid="user-table"]', { state: 'attached' });
|
||||
await page.waitForSelector('.el-table__body tr', { state: 'visible' });
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
console.log(`用户列表加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(2000);
|
||||
});
|
||||
|
||||
test('角色列表 - 使用条件等待', async ({ page }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('等待角色列表加载完成', async () => {
|
||||
await page.goto('/roles');
|
||||
await page.waitForFunction(() => {
|
||||
const rows = document.querySelectorAll('.el-table__body tr');
|
||||
return rows.length > 0;
|
||||
});
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const loadTime = endTime - startTime;
|
||||
|
||||
console.log(`角色列表加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(2000);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('选择器优化测试', () => {
|
||||
test('使用data-testid选择器', async ({ page }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('使用data-testid定位元素', async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
|
||||
const createButton = page.locator('[data-testid="create-user-button"]');
|
||||
await createButton.click();
|
||||
|
||||
await page.waitForSelector('[data-testid="user-form"]');
|
||||
await page.fill('[data-testid="username-input"]', 'testuser');
|
||||
await page.fill('[data-testid="password-input"]', 'password123');
|
||||
await page.fill('[data-testid="email-input"]', 'test@example.com');
|
||||
await page.click('[data-testid="save-button"]');
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const operationTime = endTime - startTime;
|
||||
|
||||
console.log(`data-testid选择器操作时间: ${operationTime}ms`);
|
||||
expect(operationTime).toBeLessThan(3000);
|
||||
});
|
||||
|
||||
test('选择器性能对比', async ({ page }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await test.step('对比不同选择器性能', async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
|
||||
const startTime1 = Date.now();
|
||||
const element1 = page.locator('[data-testid="create-user-button"]');
|
||||
await element1.click();
|
||||
const time1 = Date.now() - startTime1;
|
||||
|
||||
await page.click('.el-button--primary');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const startTime2 = Date.now();
|
||||
const element2 = page.locator('button.el-button--primary');
|
||||
await element2.click();
|
||||
const time2 = Date.now() - startTime2;
|
||||
|
||||
console.log(`data-testid选择器: ${time1}ms`);
|
||||
console.log(`CSS选择器: ${time2}ms`);
|
||||
expect(time1).toBeLessThan(time2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('测试数据优化测试', () => {
|
||||
test('使用缓存数据', async ({ page }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('首次加载用户列表', async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
});
|
||||
|
||||
const firstLoadTime = Date.now() - startTime;
|
||||
|
||||
await page.goto('/dashboard');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime2 = Date.now();
|
||||
|
||||
await test.step('再次加载用户列表(使用缓存)', async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
});
|
||||
|
||||
const secondLoadTime = Date.now() - startTime2;
|
||||
|
||||
console.log(`首次加载时间: ${firstLoadTime}ms`);
|
||||
console.log(`缓存加载时间: ${secondLoadTime}ms`);
|
||||
expect(secondLoadTime).toBeLessThan(firstLoadTime);
|
||||
});
|
||||
|
||||
test('优化数据准备时间', async ({ page, request }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('批量创建用户并测试性能', async () => {
|
||||
const users = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const user = {
|
||||
username: `perfuser${i}`,
|
||||
password: 'password123',
|
||||
email: `perfuser${i}@example.com`,
|
||||
roleIds: ['1']
|
||||
};
|
||||
users.push(user);
|
||||
|
||||
await request.post('http://localhost:8084/api/users', {
|
||||
data: user,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const dataPrepTime = Date.now() - startTime;
|
||||
|
||||
const startTime2 = Date.now();
|
||||
|
||||
await test.step('加载大量用户数据', async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
await page.waitForFunction(() => {
|
||||
const rows = document.querySelectorAll('.el-table__body tr');
|
||||
return rows.length >= 10;
|
||||
});
|
||||
});
|
||||
|
||||
const loadTime = Date.now() - startTime2;
|
||||
|
||||
console.log(`数据准备时间: ${dataPrepTime}ms`);
|
||||
console.log(`数据加载时间: ${loadTime}ms`);
|
||||
expect(loadTime).toBeLessThan(5000);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('测试隔离优化测试', () => {
|
||||
test('独立测试环境', async ({ page, context }) => {
|
||||
const page1 = page;
|
||||
const page2 = await context.newPage();
|
||||
|
||||
await test.step('在独立页面中执行测试', async () => {
|
||||
await page1.goto('/login');
|
||||
await page2.goto('/login');
|
||||
|
||||
await page1.fill('[data-testid="username-input"]', 'admin');
|
||||
await page2.fill('[data-testid="username-input"]', 'testuser');
|
||||
|
||||
await page1.fill('[data-testid="password-input"]', 'admin123');
|
||||
await page2.fill('[data-testid="password-input"]', 'password123');
|
||||
|
||||
await page1.click('[data-testid="login-button"]');
|
||||
await page2.click('[data-testid="login-button"]');
|
||||
|
||||
await page1.waitForURL(/.*dashboard/);
|
||||
await page2.waitForURL(/.*dashboard/);
|
||||
});
|
||||
|
||||
await test.step('验证页面隔离', async () => {
|
||||
const url1 = page1.url();
|
||||
const url2 = page2.url();
|
||||
|
||||
expect(url1).toContain('/dashboard');
|
||||
expect(url2).toContain('/dashboard');
|
||||
expect(url1).not.toBe(url2);
|
||||
});
|
||||
});
|
||||
|
||||
test('测试清理优化', async ({ page, request }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('创建测试数据', async () => {
|
||||
const user = {
|
||||
username: 'cleanupuser',
|
||||
password: 'password123',
|
||||
email: 'cleanup@example.com',
|
||||
roleIds: ['1']
|
||||
};
|
||||
|
||||
await request.post('http://localhost:8084/api/users', {
|
||||
data: user,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const createTime = Date.now() - startTime;
|
||||
|
||||
const startTime2 = Date.now();
|
||||
|
||||
await test.step('快速清理测试数据', async () => {
|
||||
const usersResponse = await request.get('http://localhost:8084/api/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
|
||||
}
|
||||
});
|
||||
|
||||
const usersData = await usersResponse.json();
|
||||
const cleanupUser = usersData.find(u => u.username === 'cleanupuser');
|
||||
if (cleanupUser) {
|
||||
await request.delete(`http://localhost:8084/api/users/${cleanupUser.id}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const cleanupTime = Date.now() - startTime2;
|
||||
|
||||
console.log(`数据创建时间: ${createTime}ms`);
|
||||
console.log(`数据清理时间: ${cleanupTime}ms`);
|
||||
expect(cleanupTime).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('并行化优化测试', () => {
|
||||
test('并行执行多个测试', async ({ page }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('并行加载多个页面', async () => {
|
||||
const promises = [
|
||||
page.goto('/users'),
|
||||
page.goto('/roles'),
|
||||
page.goto('/settings')
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const parallelTime = endTime - startTime;
|
||||
|
||||
console.log(`并行加载时间: ${parallelTime}ms`);
|
||||
expect(parallelTime).toBeLessThan(5000);
|
||||
});
|
||||
|
||||
test('并发API请求', async ({ page, request }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await test.step('并发发送多个API请求', async () => {
|
||||
const promises = [
|
||||
request.get('http://localhost:8084/api/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
|
||||
}
|
||||
}),
|
||||
request.get('http://localhost:8084/api/roles', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
|
||||
}
|
||||
}),
|
||||
request.get('http://localhost:8084/api/permissions', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const concurrentTime = endTime - startTime;
|
||||
|
||||
console.log(`并发请求时间: ${concurrentTime}ms`);
|
||||
expect(concurrentTime).toBeLessThan(2000);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('内存和资源优化测试', () => {
|
||||
test('内存使用监控', async ({ page }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
const initialMemory = await page.evaluate(() => {
|
||||
if ((window.performance as any).memory) {
|
||||
return (window.performance as any).memory.usedJSHeapSize;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
await test.step('执行多个操作', async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
|
||||
await page.goto('/roles');
|
||||
await page.waitForSelector('[data-testid="role-table"]');
|
||||
|
||||
await page.goto('/settings');
|
||||
await page.waitForSelector('[data-testid="settings-form"]');
|
||||
});
|
||||
|
||||
const finalMemory = await page.evaluate(() => {
|
||||
if ((window.performance as any).memory) {
|
||||
return (window.performance as any).memory.usedJSHeapSize;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const memoryIncrease = finalMemory - initialMemory;
|
||||
const memoryIncreaseMB = memoryIncrease / (1024 * 1024);
|
||||
|
||||
console.log(`内存增长: ${memoryIncreaseMB.toFixed(2)}MB`);
|
||||
expect(memoryIncreaseMB).toBeLessThan(50);
|
||||
});
|
||||
|
||||
test('DOM节点数量监控', async ({ page }) => {
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
await test.step('监控DOM节点数量', async () => {
|
||||
await page.goto('/users');
|
||||
await page.waitForSelector('[data-testid="user-table"]');
|
||||
|
||||
const nodeCount = await page.evaluate(() => {
|
||||
return document.querySelectorAll('*').length;
|
||||
});
|
||||
|
||||
console.log(`DOM节点数量: ${nodeCount}`);
|
||||
expect(nodeCount).toBeLessThan(5000);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,339 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 性能监控工具
|
||||
* 收集和分析测试性能数据,识别性能瓶颈
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class PerformanceMonitor {
|
||||
constructor() {
|
||||
this.performanceDataPath = path.join(process.cwd(), 'test-results', 'performance-data.json');
|
||||
this.performanceData = this.loadPerformanceData();
|
||||
this.currentSession = {
|
||||
startTime: Date.now(),
|
||||
tests: [],
|
||||
metrics: {}
|
||||
};
|
||||
}
|
||||
|
||||
loadPerformanceData() {
|
||||
try {
|
||||
if (fs.existsSync(this.performanceDataPath)) {
|
||||
return JSON.parse(fs.readFileSync(this.performanceDataPath, 'utf-8'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('加载性能数据失败:', error.message);
|
||||
}
|
||||
return {
|
||||
sessions: [],
|
||||
summary: {
|
||||
avgTestTime: 0,
|
||||
avgPageLoadTime: 0,
|
||||
avgApiTime: 0,
|
||||
totalTests: 0,
|
||||
slowTests: [],
|
||||
fastTests: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
savePerformanceData() {
|
||||
const dir = path.dirname(this.performanceDataPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(this.performanceDataPath, JSON.stringify(this.performanceData, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
startTest(testName) {
|
||||
const test = {
|
||||
name: testName,
|
||||
startTime: Date.now(),
|
||||
metrics: {
|
||||
pageLoads: [],
|
||||
apiCalls: [],
|
||||
domOperations: []
|
||||
}
|
||||
};
|
||||
this.currentSession.tests.push(test);
|
||||
return test;
|
||||
}
|
||||
|
||||
endTest(test) {
|
||||
test.endTime = Date.now();
|
||||
test.duration = test.endTime - test.startTime;
|
||||
return test;
|
||||
}
|
||||
|
||||
recordPageLoad(test, url, loadTime) {
|
||||
test.metrics.pageLoads.push({
|
||||
url,
|
||||
loadTime,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
recordApiCall(test, endpoint, duration) {
|
||||
test.metrics.apiCalls.push({
|
||||
endpoint,
|
||||
duration,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
recordDomOperation(test, operation, duration) {
|
||||
test.metrics.domOperations.push({
|
||||
operation,
|
||||
duration,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
endSession() {
|
||||
this.currentSession.endTime = Date.now();
|
||||
this.currentSession.duration = this.currentSession.endTime - this.currentSession.startTime;
|
||||
|
||||
this.performanceData.sessions.push(this.currentSession);
|
||||
this.updateSummary();
|
||||
this.savePerformanceData();
|
||||
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
updateSummary() {
|
||||
const sessions = this.performanceData.sessions;
|
||||
const allTests = sessions.flatMap(s => s.tests);
|
||||
|
||||
if (allTests.length === 0) return;
|
||||
|
||||
const totalDuration = allTests.reduce((sum, t) => sum + t.duration, 0);
|
||||
const avgTestTime = totalDuration / allTests.length;
|
||||
|
||||
const allPageLoads = allTests.flatMap(t => t.metrics.pageLoads);
|
||||
const avgPageLoadTime = allPageLoads.length > 0
|
||||
? allPageLoads.reduce((sum, p) => sum + p.loadTime, 0) / allPageLoads.length
|
||||
: 0;
|
||||
|
||||
const allApiCalls = allTests.flatMap(t => t.metrics.apiCalls);
|
||||
const avgApiTime = allApiCalls.length > 0
|
||||
? allApiCalls.reduce((sum, a) => sum + a.duration, 0) / allApiCalls.length
|
||||
: 0;
|
||||
|
||||
const sortedTests = [...allTests].sort((a, b) => b.duration - a.duration);
|
||||
const slowTests = sortedTests.slice(0, 10);
|
||||
const fastTests = sortedTests.slice(-10).reverse();
|
||||
|
||||
this.performanceData.summary = {
|
||||
avgTestTime,
|
||||
avgPageLoadTime,
|
||||
avgApiTime,
|
||||
totalTests: allTests.length,
|
||||
slowTests: slowTests.map(t => ({
|
||||
name: t.name,
|
||||
duration: t.duration
|
||||
})),
|
||||
fastTests: fastTests.map(t => ({
|
||||
name: t.name,
|
||||
duration: t.duration
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
generateReport() {
|
||||
const summary = this.performanceData.summary;
|
||||
const sessions = this.performanceData.sessions;
|
||||
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('📊 性能监控报告');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('');
|
||||
console.log(`📈 总测试数: ${summary.totalTests}`);
|
||||
console.log(`⏱️ 平均测试时间: ${this.formatDuration(summary.avgTestTime)}`);
|
||||
console.log(`🌐 平均页面加载时间: ${this.formatDuration(summary.avgPageLoadTime)}`);
|
||||
console.log(`📡 平均API响应时间: ${this.formatDuration(summary.avgApiTime)}`);
|
||||
console.log('');
|
||||
|
||||
if (summary.slowTests.length > 0) {
|
||||
console.log('🐌 最慢的10个测试:');
|
||||
summary.slowTests.forEach((test, index) => {
|
||||
console.log(` ${index + 1}. ${test.name} - ${this.formatDuration(test.duration)}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (summary.fastTests.length > 0) {
|
||||
console.log('⚡ 最快的10个测试:');
|
||||
summary.fastTests.forEach((test, index) => {
|
||||
console.log(` ${index + 1}. ${test.name} - ${this.formatDuration(test.duration)}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
this.analyzePerformanceTrends();
|
||||
this.generateRecommendations();
|
||||
}
|
||||
|
||||
analyzePerformanceTrends() {
|
||||
const sessions = this.performanceData.sessions;
|
||||
if (sessions.length < 2) return;
|
||||
|
||||
const recentSessions = sessions.slice(-5);
|
||||
const avgDurations = recentSessions.map(s => {
|
||||
const tests = s.tests;
|
||||
if (tests.length === 0) return 0;
|
||||
return tests.reduce((sum, t) => sum + t.duration, 0) / tests.length;
|
||||
});
|
||||
|
||||
const trend = this.calculateTrend(avgDurations);
|
||||
|
||||
console.log('📈 性能趋势:');
|
||||
console.log(` 趋势: ${this.getTrendEmoji(trend)} ${trend.toUpperCase()}`);
|
||||
console.log(` 最近5次平均测试时间: ${avgDurations.map(d => this.formatDuration(d)).join(', ')}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
calculateTrend(values) {
|
||||
if (values.length < 2) return 'stable';
|
||||
|
||||
const firstHalf = values.slice(0, Math.floor(values.length / 2));
|
||||
const secondHalf = values.slice(Math.floor(values.length / 2));
|
||||
|
||||
const firstAvg = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
|
||||
const secondAvg = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length;
|
||||
|
||||
const change = ((secondAvg - firstAvg) / firstAvg) * 100;
|
||||
|
||||
if (change < -10) return 'improving';
|
||||
if (change > 10) return 'degrading';
|
||||
return 'stable';
|
||||
}
|
||||
|
||||
generateRecommendations() {
|
||||
const summary = this.performanceData.summary;
|
||||
const recommendations = [];
|
||||
|
||||
if (summary.avgTestTime > 5000) {
|
||||
recommendations.push('⚠️ 平均测试时间超过5秒,建议优化测试执行效率');
|
||||
}
|
||||
|
||||
if (summary.avgPageLoadTime > 2000) {
|
||||
recommendations.push('⚠️ 平均页面加载时间超过2秒,建议优化页面性能');
|
||||
}
|
||||
|
||||
if (summary.avgApiTime > 1000) {
|
||||
recommendations.push('⚠️ 平均API响应时间超过1秒,建议优化API性能');
|
||||
}
|
||||
|
||||
const slowTestsCount = summary.slowTests.filter(t => t.duration > 10000).length;
|
||||
if (slowTestsCount > 5) {
|
||||
recommendations.push(`⚠️ 有${slowTestsCount}个测试执行时间超过10秒,建议重点优化`);
|
||||
}
|
||||
|
||||
if (recommendations.length > 0) {
|
||||
console.log('💡 性能优化建议:');
|
||||
recommendations.forEach(rec => {
|
||||
console.log(` ${rec}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
getTrendEmoji(trend) {
|
||||
switch (trend) {
|
||||
case 'improving':
|
||||
return '📈';
|
||||
case 'degrading':
|
||||
return '📉';
|
||||
default:
|
||||
return '➡️';
|
||||
}
|
||||
}
|
||||
|
||||
formatDuration(ms) {
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
} else if (ms < 60000) {
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
} else {
|
||||
return `${(ms / 60000).toFixed(1)}m`;
|
||||
}
|
||||
}
|
||||
|
||||
exportData(filePath) {
|
||||
const exportPath = filePath || 'performance-data-export.json';
|
||||
fs.writeFileSync(exportPath, JSON.stringify(this.performanceData, null, 2), 'utf-8');
|
||||
console.log(`✅ 性能数据已导出到: ${exportPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行接口
|
||||
if (require.main === module) {
|
||||
const monitor = new PerformanceMonitor();
|
||||
const command = process.argv[2];
|
||||
|
||||
switch (command) {
|
||||
case 'report':
|
||||
monitor.generateReport();
|
||||
break;
|
||||
|
||||
case 'export':
|
||||
const exportFile = process.argv[3];
|
||||
monitor.exportData(exportFile);
|
||||
break;
|
||||
|
||||
case 'start':
|
||||
const testName = process.argv[3];
|
||||
if (testName) {
|
||||
const test = monitor.startTest(testName);
|
||||
console.log(`✅ 测试已启动: ${testName}`);
|
||||
console.log(`测试ID: ${monitor.currentSession.tests.length - 1}`);
|
||||
} else {
|
||||
console.error('❌ 错误: 请提供测试名称');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'end':
|
||||
const testId = parseInt(process.argv[3]);
|
||||
if (!isNaN(testId)) {
|
||||
const test = monitor.currentSession.tests[testId];
|
||||
if (test) {
|
||||
monitor.endTest(test);
|
||||
console.log(`✅ 测试已结束: ${test.name}`);
|
||||
console.log(`执行时间: ${monitor.formatDuration(test.duration)}`);
|
||||
} else {
|
||||
console.error('❌ 错误: 测试ID不存在');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 错误: 请提供有效的测试ID');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'session':
|
||||
monitor.endSession();
|
||||
console.log('✅ 测试会话已结束');
|
||||
console.log(`会话时长: ${monitor.formatDuration(monitor.currentSession.duration)}`);
|
||||
console.log(`测试数量: ${monitor.currentSession.tests.length}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('性能监控工具');
|
||||
console.log('');
|
||||
console.log('用法:');
|
||||
console.log(' node performanceMonitor.js report - 生成性能报告');
|
||||
console.log(' node performanceMonitor.js export [file.json] - 导出性能数据');
|
||||
console.log(' node performanceMonitor.js start <testName> - 启动测试监控');
|
||||
console.log(' node performanceMonitor.js end <testId> - 结束测试监控');
|
||||
console.log(' node performanceMonitor.js session - 结束测试会话');
|
||||
console.log('');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PerformanceMonitor;
|
||||
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 质量门禁检查工具
|
||||
* 定义和执行自动化质量标准,阻止低质量代码合并
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class QualityGate {
|
||||
constructor() {
|
||||
this.qualityStandards = {
|
||||
passRate: 95, // 通过率必须 >= 95%
|
||||
flakyRate: 5, // 不稳定测试比例必须 <= 5%
|
||||
maxDuration: 600000, // 总测试时间必须 <= 10分钟
|
||||
maxFailedTests: 5, // 失败测试数量必须 <= 5
|
||||
maxSlowTests: 10, // 慢速测试数量必须 <= 10
|
||||
};
|
||||
|
||||
this.checks = [];
|
||||
this.passed = true;
|
||||
this.warnings = [];
|
||||
this.errors = [];
|
||||
}
|
||||
|
||||
checkPassRate(results) {
|
||||
const passRate = results.summary?.passRate || 0;
|
||||
const threshold = this.qualityStandards.passRate;
|
||||
|
||||
if (passRate < threshold) {
|
||||
this.errors.push({
|
||||
check: '通过率检查',
|
||||
message: `测试通过率 ${passRate.toFixed(2)}% 低于标准 ${threshold}%`,
|
||||
actual: passRate,
|
||||
threshold: threshold,
|
||||
status: 'failed',
|
||||
});
|
||||
this.passed = false;
|
||||
} else {
|
||||
this.checks.push({
|
||||
check: '通过率检查',
|
||||
message: `测试通过率 ${passRate.toFixed(2)}% 符合标准`,
|
||||
actual: passRate,
|
||||
threshold: threshold,
|
||||
status: 'passed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkFlakyRate(results) {
|
||||
const flakyRate = results.summary?.flakyRate || 0;
|
||||
const threshold = this.qualityStandards.flakyRate;
|
||||
|
||||
if (flakyRate > threshold) {
|
||||
this.warnings.push({
|
||||
check: '不稳定测试检查',
|
||||
message: `不稳定测试比例 ${flakyRate.toFixed(2)}% 超过标准 ${threshold}%`,
|
||||
actual: flakyRate,
|
||||
threshold: threshold,
|
||||
status: 'warning',
|
||||
});
|
||||
} else {
|
||||
this.checks.push({
|
||||
check: '不稳定测试检查',
|
||||
message: `不稳定测试比例 ${flakyRate.toFixed(2)}% 符合标准`,
|
||||
actual: flakyRate,
|
||||
threshold: threshold,
|
||||
status: 'passed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkDuration(results) {
|
||||
const duration = results.summary?.totalDuration || 0;
|
||||
const threshold = this.qualityStandards.maxDuration;
|
||||
|
||||
if (duration > threshold) {
|
||||
this.warnings.push({
|
||||
check: '测试耗时检查',
|
||||
message: `测试总耗时 ${this.formatDuration(duration)} 超过标准 ${this.formatDuration(threshold)}`,
|
||||
actual: duration,
|
||||
threshold: threshold,
|
||||
status: 'warning',
|
||||
});
|
||||
} else {
|
||||
this.checks.push({
|
||||
check: '测试耗时检查',
|
||||
message: `测试总耗时 ${this.formatDuration(duration)} 符合标准`,
|
||||
actual: duration,
|
||||
threshold: threshold,
|
||||
status: 'passed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkFailedTests(results) {
|
||||
const failedCount = results.failedTests?.length || 0;
|
||||
const threshold = this.qualityStandards.maxFailedTests;
|
||||
|
||||
if (failedCount > threshold) {
|
||||
this.errors.push({
|
||||
check: '失败测试数量检查',
|
||||
message: `失败测试数量 ${failedCount} 超过标准 ${threshold}`,
|
||||
actual: failedCount,
|
||||
threshold: threshold,
|
||||
status: 'failed',
|
||||
});
|
||||
this.passed = false;
|
||||
} else {
|
||||
this.checks.push({
|
||||
check: '失败测试数量检查',
|
||||
message: `失败测试数量 ${failedCount} 符合标准`,
|
||||
actual: failedCount,
|
||||
threshold: threshold,
|
||||
status: 'passed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkSlowTests(results) {
|
||||
const slowCount = results.slowestTests?.length || 0;
|
||||
const threshold = this.qualityStandards.maxSlowTests;
|
||||
|
||||
if (slowCount > threshold) {
|
||||
this.warnings.push({
|
||||
check: '慢速测试数量检查',
|
||||
message: `慢速测试数量 ${slowCount} 超过标准 ${threshold}`,
|
||||
actual: slowCount,
|
||||
threshold: threshold,
|
||||
status: 'warning',
|
||||
});
|
||||
} else {
|
||||
this.checks.push({
|
||||
check: '慢速测试数量检查',
|
||||
message: `慢速测试数量 ${slowCount} 符合标准`,
|
||||
actual: slowCount,
|
||||
threshold: threshold,
|
||||
status: 'passed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkCriticalTests(results) {
|
||||
const criticalTests = results.failedTests?.filter(test => {
|
||||
const title = test.title.toLowerCase();
|
||||
return title.includes('登录') || title.includes('认证') || title.includes('安全');
|
||||
}) || [];
|
||||
|
||||
if (criticalTests.length > 0) {
|
||||
this.errors.push({
|
||||
check: '关键功能测试检查',
|
||||
message: `关键功能测试失败: ${criticalTests.map(t => t.title).join(', ')}`,
|
||||
actual: criticalTests.length,
|
||||
threshold: 0,
|
||||
status: 'failed',
|
||||
});
|
||||
this.passed = false;
|
||||
} else {
|
||||
this.checks.push({
|
||||
check: '关键功能测试检查',
|
||||
message: '所有关键功能测试通过',
|
||||
actual: 0,
|
||||
threshold: 0,
|
||||
status: 'passed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
execute(results) {
|
||||
this.checkPassRate(results);
|
||||
this.checkFlakyRate(results);
|
||||
this.checkDuration(results);
|
||||
this.checkFailedTests(results);
|
||||
this.checkSlowTests(results);
|
||||
this.checkCriticalTests(results);
|
||||
|
||||
return this.generateReport();
|
||||
}
|
||||
|
||||
generateReport() {
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
passed: this.passed,
|
||||
summary: {
|
||||
total: this.checks.length,
|
||||
passed: this.checks.filter(c => c.status === 'passed').length,
|
||||
warnings: this.warnings.length,
|
||||
errors: this.errors.length,
|
||||
},
|
||||
checks: this.checks,
|
||||
warnings: this.warnings,
|
||||
errors: this.errors,
|
||||
};
|
||||
|
||||
this.printReport(report);
|
||||
this.saveReport(report);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
printReport(report) {
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('🚪 质量门禁检查报告');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('');
|
||||
console.log(`📊 检查时间: ${new Date(report.timestamp).toLocaleString('zh-CN')}`);
|
||||
console.log(`📈 检查结果: ${report.passed ? '✅ 通过' : '❌ 失败'}`);
|
||||
console.log('');
|
||||
console.log(`📋 检查统计:`);
|
||||
console.log(` - 总检查项: ${report.summary.total}`);
|
||||
console.log(` - 通过: ${report.summary.passed}`);
|
||||
console.log(` - 警告: ${report.summary.warnings}`);
|
||||
console.log(` - 错误: ${report.summary.errors}`);
|
||||
console.log('');
|
||||
|
||||
if (report.checks.length > 0) {
|
||||
console.log('✅ 通过的检查:');
|
||||
report.checks.forEach(check => {
|
||||
console.log(` ✓ ${check.check}: ${check.message}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (report.warnings.length > 0) {
|
||||
console.log('⚠️ 警告:');
|
||||
report.warnings.forEach(warning => {
|
||||
console.log(` ⚠️ ${warning.check}: ${warning.message}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (report.errors.length > 0) {
|
||||
console.log('❌ 错误:');
|
||||
report.errors.forEach(error => {
|
||||
console.log(` ❌ ${error.check}: ${error.message}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('');
|
||||
|
||||
if (!report.passed) {
|
||||
console.error('❌ 质量门禁检查失败!请修复错误后重试。');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
saveReport(report) {
|
||||
const dir = path.join(process.cwd(), 'test-results');
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const reportPath = path.join(dir, 'quality-gate-report.json');
|
||||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
|
||||
console.log(`📄 质量门禁报告已保存: ${reportPath}`);
|
||||
}
|
||||
|
||||
formatDuration(ms) {
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
} else if (ms < 60000) {
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
} else {
|
||||
return `${(ms / 60000).toFixed(1)}m`;
|
||||
}
|
||||
}
|
||||
|
||||
setStandard(standard, value) {
|
||||
if (this.qualityStandards.hasOwnProperty(standard)) {
|
||||
this.qualityStandards[standard] = value;
|
||||
console.log(`✅ 质量标准已更新: ${standard} = ${value}`);
|
||||
} else {
|
||||
console.error(`❌ 错误: 未知的质量标准 ${standard}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
getStandards() {
|
||||
console.log('当前质量标准:');
|
||||
console.log('');
|
||||
console.log(` 通过率: >= ${this.qualityStandards.passRate}%`);
|
||||
console.log(` 不稳定测试比例: <= ${this.qualityStandards.flakyRate}%`);
|
||||
console.log(` 最大测试时间: <= ${this.formatDuration(this.qualityStandards.maxDuration)}`);
|
||||
console.log(` 最大失败测试数: <= ${this.qualityStandards.maxFailedTests}`);
|
||||
console.log(` 最大慢速测试数: <= ${this.qualityStandards.maxSlowTests}`);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行接口
|
||||
if (require.main === module) {
|
||||
const qualityGate = new QualityGate();
|
||||
const command = process.argv[2];
|
||||
|
||||
switch (command) {
|
||||
case 'check':
|
||||
const resultsFile = process.argv[3];
|
||||
if (resultsFile && fs.existsSync(resultsFile)) {
|
||||
const results = JSON.parse(fs.readFileSync(resultsFile, 'utf-8'));
|
||||
qualityGate.execute(results);
|
||||
} else {
|
||||
console.error('❌ 错误: 请提供有效的测试结果文件');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'set':
|
||||
const standard = process.argv[3];
|
||||
const value = parseFloat(process.argv[4]);
|
||||
if (standard && !isNaN(value)) {
|
||||
qualityGate.setStandard(standard, value);
|
||||
} else {
|
||||
console.error('❌ 错误: 请提供有效的标准和数值');
|
||||
console.error('用法: node qualityGate.js set <standard> <value>');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'standards':
|
||||
qualityGate.getStandards();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('质量门禁检查工具');
|
||||
console.log('');
|
||||
console.log('用法:');
|
||||
console.log(' node qualityGate.js check <results.json> - 执行质量门禁检查');
|
||||
console.log(' node qualityGate.js set <standard> <value> - 设置质量标准');
|
||||
console.log(' node qualityGate.js standards - 显示当前质量标准');
|
||||
console.log('');
|
||||
console.log('质量标准:');
|
||||
console.log(' - passRate: 通过率 (%)');
|
||||
console.log(' - flakyRate: 不稳定测试比例 (%)');
|
||||
console.log(' - maxDuration: 最大测试时间 (ms)');
|
||||
console.log(' - maxFailedTests: 最大失败测试数');
|
||||
console.log(' - maxSlowTests: 最大慢速测试数');
|
||||
console.log('');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = QualityGate;
|
||||
@@ -0,0 +1,386 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { RoleManagementPage } from './pages/RoleManagementPage';
|
||||
import { TestDataManager } from './utils/testDataManager';
|
||||
import { TestHelper } from './utils/testHelper';
|
||||
|
||||
test.describe('角色管理异常场景测试', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
let roleManagementPage: RoleManagementPage;
|
||||
let testRole: any;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
roleManagementPage = new RoleManagementPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page, request }) => {
|
||||
await TestHelper.clearAllStorage(page);
|
||||
|
||||
if (testRole) {
|
||||
await TestDataManager.deleteTestRole(request, testRole.roleKey);
|
||||
testRole = null;
|
||||
}
|
||||
});
|
||||
|
||||
test('创建角色 - 重复角色键', async ({ page }) => {
|
||||
await test.step('导航到角色管理页面', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试创建重复角色键的角色', async () => {
|
||||
await roleManagementPage.clickCreateRole();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const roleData = {
|
||||
roleName: '管理员',
|
||||
roleKey: 'admin',
|
||||
roleSort: '1',
|
||||
status: '1',
|
||||
remark: '重复角色键',
|
||||
};
|
||||
|
||||
await roleManagementPage.fillRoleForm(roleData);
|
||||
await roleManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证错误消息', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('角色键已存在');
|
||||
});
|
||||
});
|
||||
|
||||
test('创建角色 - 缺少必填字段', async ({ page }) => {
|
||||
await test.step('导航到角色管理页面', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试创建缺少必填字段的角色', async () => {
|
||||
await roleManagementPage.clickCreateRole();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const roleData = {
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
roleSort: '',
|
||||
status: '',
|
||||
remark: '',
|
||||
};
|
||||
|
||||
await roleManagementPage.fillRoleForm(roleData);
|
||||
await roleManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证表单验证', async () => {
|
||||
const submitButton = page.locator('.el-dialog__footer button[type="submit"]');
|
||||
const isDisabled = await submitButton.evaluate(el => (el as HTMLButtonElement).disabled);
|
||||
expect(isDisabled).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('创建角色 - 无效角色键格式', async ({ page }) => {
|
||||
await test.step('导航到角色管理页面', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试创建无效角色键格式的角色', async () => {
|
||||
await roleManagementPage.clickCreateRole();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const roleData = {
|
||||
roleName: `测试角色_${Date.now()}`,
|
||||
roleKey: '无效角色键!@#',
|
||||
roleSort: '1',
|
||||
status: '1',
|
||||
remark: '无效角色键格式',
|
||||
};
|
||||
|
||||
await roleManagementPage.fillRoleForm(roleData);
|
||||
await roleManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证角色键格式错误', async () => {
|
||||
const roleKeyInput = page.locator('input[name="roleKey"]');
|
||||
const hasError = await roleKeyInput.evaluate(el => el.classList.contains('is-error'));
|
||||
expect(hasError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('编辑角色 - 不存在的角色ID', async ({ page }) => {
|
||||
await test.step('尝试编辑不存在的角色', async () => {
|
||||
await page.goto('/roles/999999/edit');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('验证404错误或重定向', async () => {
|
||||
const currentUrl = page.url();
|
||||
expect(currentUrl).toMatch(/(404|roles)/);
|
||||
});
|
||||
});
|
||||
|
||||
test('删除角色 - 不存在的角色ID', async ({ page, request }) => {
|
||||
await test.step('尝试删除不存在的角色', async () => {
|
||||
const response = await request.delete('http://localhost:8084/api/roles/999999');
|
||||
expect(response.status()).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
test('删除角色 - 系统内置角色', async ({ page }) => {
|
||||
await test.step('导航到角色管理页面', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试删除系统内置角色', async () => {
|
||||
const adminRoleRow = page.locator('table tbody tr').filter({ hasText: 'admin' }).first();
|
||||
const deleteButton = adminRoleRow.locator('.delete-button');
|
||||
|
||||
if (await deleteButton.count() > 0) {
|
||||
await deleteButton.click();
|
||||
await TestHelper.waitForElementVisible(page, '.el-message-box');
|
||||
|
||||
await page.click('.el-message-box__btns .el-button--primary');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证系统内置角色不能删除', async () => {
|
||||
const adminRoleRow = page.locator('table tbody tr').filter({ hasText: 'admin' }).first();
|
||||
await expect(adminRoleRow).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('搜索角色 - 空搜索条件', async ({ page }) => {
|
||||
await test.step('导航到角色管理页面', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('执行空搜索', async () => {
|
||||
const searchInput = page.locator('input[placeholder*="搜索"]').or(page.locator('.search-input'));
|
||||
if (await searchInput.count() > 0) {
|
||||
await searchInput.fill('');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证显示所有角色', async () => {
|
||||
const roleCount = await page.locator('.el-table__body tr').count();
|
||||
expect(roleCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('搜索角色 - 不存在的角色名', async ({ page }) => {
|
||||
await test.step('导航到角色管理页面', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('搜索不存在的角色', async () => {
|
||||
const searchInput = page.locator('input[placeholder*="搜索"]').or(page.locator('.search-input'));
|
||||
if (await searchInput.count() > 0) {
|
||||
await searchInput.fill('nonexistentrole123456');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证无结果', async () => {
|
||||
const roleCount = await page.locator('.el-table__body tr').count();
|
||||
expect(roleCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('分配权限 - 角色不存在', async ({ page, request }) => {
|
||||
await test.step('尝试为不存在的角色分配权限', async () => {
|
||||
const response = await request.post('http://localhost:8084/api/roles/999999/permissions', {
|
||||
data: { permissions: ['user:view'] }
|
||||
});
|
||||
expect(response.status()).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
test('分配权限 - 无效权限标识', async ({ page }) => {
|
||||
await test.step('导航到角色管理页面', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试分配无效权限', async () => {
|
||||
const firstRow = page.locator('.el-table__body tr').first();
|
||||
await firstRow.click();
|
||||
await TestHelper.waitForElementVisible(page, '.permission-dialog');
|
||||
|
||||
const invalidPermission = page.locator('.permission-item').filter({ hasText: 'invalid:permission' });
|
||||
if (await invalidPermission.count() > 0) {
|
||||
await invalidPermission.click();
|
||||
await page.click('.permission-dialog .save-button');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证权限分配失败', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
});
|
||||
});
|
||||
|
||||
test('角色状态切换 - 禁用后用户无法登录', async ({ page, request }) => {
|
||||
testRole = TestDataManager.generateTestRole();
|
||||
await TestDataManager.createTestRole(request, testRole);
|
||||
|
||||
const testUser = TestDataManager.generateTestUser({ roleIds: [testRole.id] });
|
||||
await TestDataManager.createTestUser(request, testUser);
|
||||
|
||||
await test.step('禁用角色', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
|
||||
const roleRow = page.locator('table tbody tr').filter({ hasText: testRole.roleName }).first();
|
||||
await roleRow.locator('.status-toggle').click();
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
});
|
||||
|
||||
await test.step('验证用户无法登录', async () => {
|
||||
await loginPage.logout();
|
||||
await loginPage.goto();
|
||||
|
||||
const testUser = TestDataManager.generateTestUser();
|
||||
await loginPage.login(testUser.username, testUser.password);
|
||||
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const currentUrl = page.url();
|
||||
expect(currentUrl).toContain('/login');
|
||||
});
|
||||
});
|
||||
|
||||
test('批量删除角色 - 未选择角色', async ({ page }) => {
|
||||
await test.step('导航到角色管理页面', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试批量删除未选择的角色', async () => {
|
||||
const batchDeleteButton = page.locator('button:has-text("批量删除")');
|
||||
if (await batchDeleteButton.count() > 0) {
|
||||
await batchDeleteButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证提示消息', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
});
|
||||
});
|
||||
|
||||
test('批量删除角色 - 包含系统内置角色', async ({ page }) => {
|
||||
await test.step('导航到角色管理页面', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('选择包含系统内置角色的多个角色', async () => {
|
||||
const adminRoleRow = page.locator('table tbody tr').filter({ hasText: 'admin' }).first();
|
||||
await adminRoleRow.locator('input[type="checkbox"]').check();
|
||||
|
||||
const otherRoleRow = page.locator('table tbody tr').nth(1);
|
||||
if (await otherRoleRow.count() > 0) {
|
||||
await otherRoleRow.locator('input[type="checkbox"]').check();
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('尝试批量删除', async () => {
|
||||
const batchDeleteButton = page.locator('button:has-text("批量删除")');
|
||||
if (await batchDeleteButton.count() > 0) {
|
||||
await batchDeleteButton.click();
|
||||
await TestHelper.waitForElementVisible(page, '.el-message-box');
|
||||
|
||||
await page.click('.el-message-box__btns .el-button--primary');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证系统内置角色未被删除', async () => {
|
||||
const adminRoleRow = page.locator('table tbody tr').filter({ hasText: 'admin' }).first();
|
||||
await expect(adminRoleRow).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('网络错误 - 创建角色时断网', async ({ page }) => {
|
||||
await test.step('导航到角色管理页面', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('模拟网络错误', async () => {
|
||||
await page.route('**/api/roles', route => route.abort('failed'));
|
||||
});
|
||||
|
||||
await test.step('尝试创建角色', async () => {
|
||||
await roleManagementPage.clickCreateRole();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const roleData = {
|
||||
roleName: `测试角色_${Date.now()}`,
|
||||
roleKey: `test_role_${Date.now()}`,
|
||||
roleSort: '1',
|
||||
status: '1',
|
||||
remark: '测试角色',
|
||||
};
|
||||
|
||||
await roleManagementPage.fillRoleForm(roleData);
|
||||
await roleManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证网络错误提示', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
});
|
||||
});
|
||||
|
||||
test('并发操作 - 同时编辑同一角色', async ({ page, context }) => {
|
||||
const page1 = page;
|
||||
const page2 = await context.newPage();
|
||||
|
||||
await page1.goto('/roles');
|
||||
await page2.goto('/roles');
|
||||
|
||||
await TestHelper.waitForPageLoad(page1);
|
||||
await TestHelper.waitForPageLoad(page2);
|
||||
|
||||
const firstRow1 = page1.locator('.el-table__body tr').first();
|
||||
const firstRow2 = page2.locator('.el-table__body tr').first();
|
||||
|
||||
await firstRow1.click();
|
||||
await firstRow2.click();
|
||||
|
||||
await TestHelper.waitForElementVisible(page1, '.el-dialog');
|
||||
await TestHelper.waitForElementVisible(page2, '.el-dialog');
|
||||
|
||||
await page1.fill('input[name="roleName"]', '并发编辑1');
|
||||
await page2.fill('input[name="roleName"]', '并发编辑2');
|
||||
|
||||
await page1.click('.el-dialog__footer button[type="submit"]');
|
||||
await TestHelper.waitForPageLoad(page1);
|
||||
|
||||
await page2.click('.el-dialog__footer button[type="submit"]');
|
||||
await TestHelper.waitForPageLoad(page2);
|
||||
|
||||
await page1.goto('/roles');
|
||||
await page2.goto('/roles');
|
||||
|
||||
await TestHelper.waitForPageLoad(page1);
|
||||
await TestHelper.waitForPageLoad(page2);
|
||||
|
||||
const errorMessage1 = await TestHelper.getElementText(page1, '.el-message__content');
|
||||
const errorMessage2 = await TestHelper.getElementText(page2, '.el-message__content');
|
||||
|
||||
expect(errorMessage1 || errorMessage2).toContain('数据已被其他用户修改');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,369 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 测试趋势分析工具
|
||||
* 收集和分析历史测试数据,识别测试质量变化趋势
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
class TestTrendAnalyzer {
|
||||
constructor() {
|
||||
this.trendDataPath = path.join(process.cwd(), 'test-results', 'trends.json');
|
||||
this.historyDataPath = path.join(process.cwd(), 'test-results', 'history');
|
||||
this.trendData = this.loadTrendData();
|
||||
}
|
||||
|
||||
loadTrendData() {
|
||||
try {
|
||||
if (fs.existsSync(this.trendDataPath)) {
|
||||
return JSON.parse(fs.readFileSync(this.trendDataPath, 'utf-8'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('加载趋势数据失败:', error.message);
|
||||
}
|
||||
return {
|
||||
runs: [],
|
||||
summary: {
|
||||
totalRuns: 0,
|
||||
avgPassRate: 0,
|
||||
avgDuration: 0,
|
||||
trend: 'stable',
|
||||
lastUpdated: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
saveTrendData() {
|
||||
const dir = path.dirname(this.trendDataPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(this.trendDataPath, JSON.stringify(this.trendData, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
addTestRun(testResults) {
|
||||
const run = {
|
||||
timestamp: new Date().toISOString(),
|
||||
summary: {
|
||||
total: testResults.summary?.total || 0,
|
||||
passed: testResults.summary?.passed || 0,
|
||||
failed: testResults.summary?.failed || 0,
|
||||
skipped: testResults.summary?.skipped || 0,
|
||||
flaky: testResults.summary?.flaky || 0,
|
||||
passRate: testResults.summary?.passRate || 0,
|
||||
failRate: testResults.summary?.failRate || 0,
|
||||
skipRate: testResults.summary?.skipRate || 0,
|
||||
flakyRate: testResults.summary?.flakyRate || 0,
|
||||
totalDuration: testResults.summary?.totalDuration || 0,
|
||||
avgDuration: testResults.summary?.avgDuration || 0,
|
||||
},
|
||||
failedTests: testResults.failedTests || [],
|
||||
slowestTests: testResults.slowestTests || [],
|
||||
environment: this.getEnvironmentInfo(),
|
||||
};
|
||||
|
||||
this.trendData.runs.push(run);
|
||||
this.updateSummary();
|
||||
this.saveTrendData();
|
||||
this.saveHistory(run);
|
||||
|
||||
return run;
|
||||
}
|
||||
|
||||
updateSummary() {
|
||||
const runs = this.trendData.runs;
|
||||
const recentRuns = runs.slice(-10);
|
||||
|
||||
this.trendData.summary.totalRuns = runs.length;
|
||||
this.trendData.summary.avgPassRate = this.calculateAverage(recentRuns, 'passRate');
|
||||
this.trendData.summary.avgDuration = this.calculateAverage(recentRuns, 'totalDuration');
|
||||
this.trendData.summary.trend = this.analyzeTrend();
|
||||
this.trendData.summary.lastUpdated = new Date().toISOString();
|
||||
}
|
||||
|
||||
calculateAverage(runs, field) {
|
||||
if (runs.length === 0) return 0;
|
||||
const sum = runs.reduce((acc, run) => acc + (run.summary[field] || 0), 0);
|
||||
return sum / runs.length;
|
||||
}
|
||||
|
||||
analyzeTrend() {
|
||||
const runs = this.trendData.runs;
|
||||
if (runs.length < 3) return 'stable';
|
||||
|
||||
const recentPassRates = runs.slice(-5).map(r => r.summary.passRate);
|
||||
const avgPassRate = recentPassRates.reduce((a, b) => a + b, 0) / recentPassRates.length;
|
||||
const latestPassRate = recentPassRates[recentPassRates.length - 1];
|
||||
|
||||
if (latestPassRate < avgPassRate - 5) {
|
||||
return 'degrading';
|
||||
} else if (latestPassRate > avgPassRate + 5) {
|
||||
return 'improving';
|
||||
} else {
|
||||
return 'stable';
|
||||
}
|
||||
}
|
||||
|
||||
saveHistory(run) {
|
||||
const dir = this.historyDataPath;
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const filename = `run-${Date.now()}.json`;
|
||||
const filepath = path.join(dir, filename);
|
||||
fs.writeFileSync(filepath, JSON.stringify(run, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
getEnvironmentInfo() {
|
||||
return {
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
nodeVersion: process.version,
|
||||
hostname: os.hostname(),
|
||||
cpus: os.cpus().length,
|
||||
totalMemory: os.totalmem(),
|
||||
freeMemory: os.freemem(),
|
||||
};
|
||||
}
|
||||
|
||||
generateTrendReport() {
|
||||
const runs = this.trendData.runs;
|
||||
const summary = this.trendData.summary;
|
||||
|
||||
if (runs.length === 0) {
|
||||
console.log('暂无测试数据');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('📈 测试趋势分析报告');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('');
|
||||
console.log(`📊 总运行次数: ${summary.totalRuns}`);
|
||||
console.log(`📈 平均通过率: ${summary.avgPassRate.toFixed(2)}%`);
|
||||
console.log(`⏱️ 平均耗时: ${this.formatDuration(summary.avgDuration)}`);
|
||||
console.log(`📉 趋势: ${this.getTrendEmoji(summary.trend)} ${summary.trend.toUpperCase()}`);
|
||||
console.log('');
|
||||
|
||||
const recentRuns = runs.slice(-10);
|
||||
console.log('📅 最近10次运行:');
|
||||
recentRuns.forEach((run, index) => {
|
||||
const date = new Date(run.timestamp);
|
||||
const dateStr = date.toLocaleString('zh-CN');
|
||||
const passRate = run.summary.passRate.toFixed(2);
|
||||
const duration = this.formatDuration(run.summary.totalDuration);
|
||||
console.log(` ${index + 1}. ${dateStr} - 通过率: ${passRate}% - 耗时: ${duration}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
this.analyzeFlakyTests();
|
||||
this.analyzeSlowTests();
|
||||
this.analyzeFailedTests();
|
||||
this.generateRecommendations();
|
||||
}
|
||||
|
||||
analyzeFlakyTests() {
|
||||
const runs = this.trendData.runs;
|
||||
const flakyTestMap = new Map();
|
||||
|
||||
runs.forEach(run => {
|
||||
run.failedTests.forEach(test => {
|
||||
const key = `${test.title}`;
|
||||
if (!flakyTestMap.has(key)) {
|
||||
flakyTestMap.set(key, {
|
||||
title: test.title,
|
||||
failures: 0,
|
||||
runs: 0,
|
||||
});
|
||||
}
|
||||
flakyTestMap.get(key).failures++;
|
||||
});
|
||||
flakyTestMap.forEach(test => {
|
||||
test.runs++;
|
||||
});
|
||||
});
|
||||
|
||||
const flakyTests = Array.from(flakyTestMap.values())
|
||||
.filter(test => test.failures >= 2)
|
||||
.sort((a, b) => b.failures - a.failures)
|
||||
.slice(0, 10);
|
||||
|
||||
if (flakyTests.length > 0) {
|
||||
console.log('🔄 不稳定测试 (失败2次以上):');
|
||||
flakyTests.forEach((test, index) => {
|
||||
const failRate = ((test.failures / test.runs) * 100).toFixed(2);
|
||||
console.log(` ${index + 1}. ${test.title} - 失败: ${test.failures}/${test.runs} (${failRate}%)`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
analyzeSlowTests() {
|
||||
const runs = this.trendData.runs;
|
||||
const slowTestMap = new Map();
|
||||
|
||||
runs.forEach(run => {
|
||||
run.slowestTests.forEach(test => {
|
||||
const key = `${test.title}`;
|
||||
if (!slowTestMap.has(key)) {
|
||||
slowTestMap.set(key, {
|
||||
title: test.title,
|
||||
durations: [],
|
||||
});
|
||||
}
|
||||
slowTestMap.get(key).durations.push(test.duration);
|
||||
});
|
||||
});
|
||||
|
||||
const slowTests = Array.from(slowTestMap.values())
|
||||
.map(test => ({
|
||||
title: test.title,
|
||||
avgDuration: test.durations.reduce((a, b) => a + b, 0) / test.durations.length,
|
||||
maxDuration: Math.max(...test.durations),
|
||||
runs: test.durations.length,
|
||||
}))
|
||||
.sort((a, b) => b.avgDuration - a.avgDuration)
|
||||
.slice(0, 10);
|
||||
|
||||
if (slowTests.length > 0) {
|
||||
console.log('🐌 最慢的测试 (平均耗时):');
|
||||
slowTests.forEach((test, index) => {
|
||||
console.log(` ${index + 1}. ${test.title} - 平均: ${this.formatDuration(test.avgDuration)} - 最大: ${this.formatDuration(test.maxDuration)}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
analyzeFailedTests() {
|
||||
const runs = this.trendData.runs;
|
||||
const failedTestMap = new Map();
|
||||
|
||||
runs.forEach(run => {
|
||||
run.failedTests.forEach(test => {
|
||||
const key = `${test.title}`;
|
||||
if (!failedTestMap.has(key)) {
|
||||
failedTestMap.set(key, {
|
||||
title: test.title,
|
||||
failures: 0,
|
||||
lastFailure: null,
|
||||
errorMessages: new Set(),
|
||||
});
|
||||
}
|
||||
failedTestMap.get(key).failures++;
|
||||
failedTestMap.get(key).lastFailure = run.timestamp;
|
||||
if (test.error) {
|
||||
failedTestMap.get(key).errorMessages.add(test.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const failedTests = Array.from(failedTestMap.values())
|
||||
.sort((a, b) => b.failures - a.failures)
|
||||
.slice(0, 10);
|
||||
|
||||
if (failedTests.length > 0) {
|
||||
console.log('❌ 最常失败的测试:');
|
||||
failedTests.forEach((test, index) => {
|
||||
const lastFailure = new Date(test.lastFailure).toLocaleString('zh-CN');
|
||||
console.log(` ${index + 1}. ${test.title} - 失败: ${test.failures}次 - 最后失败: ${lastFailure}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
generateRecommendations() {
|
||||
const summary = this.trendData.summary;
|
||||
const runs = this.trendData.runs;
|
||||
const recommendations = [];
|
||||
|
||||
if (summary.trend === 'degrading') {
|
||||
recommendations.push('⚠️ 测试通过率呈下降趋势,建议检查最近的代码变更');
|
||||
}
|
||||
|
||||
const recentFlakyRate = runs.slice(-5).reduce((sum, run) => sum + run.summary.flakyRate, 0) / 5;
|
||||
if (recentFlakyRate > 10) {
|
||||
recommendations.push('🔄 不稳定测试比例较高,建议优化测试稳定性');
|
||||
}
|
||||
|
||||
const recentAvgDuration = runs.slice(-5).reduce((sum, run) => sum + run.summary.totalDuration, 0) / 5;
|
||||
if (recentAvgDuration > 300000) {
|
||||
recommendations.push('⏱️ 测试执行时间较长,建议优化测试性能');
|
||||
}
|
||||
|
||||
if (recommendations.length > 0) {
|
||||
console.log('💡 改进建议:');
|
||||
recommendations.forEach(rec => {
|
||||
console.log(` ${rec}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
getTrendEmoji(trend) {
|
||||
switch (trend) {
|
||||
case 'improving':
|
||||
return '📈';
|
||||
case 'degrading':
|
||||
return '📉';
|
||||
default:
|
||||
return '➡️';
|
||||
}
|
||||
}
|
||||
|
||||
formatDuration(ms) {
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
} else if (ms < 60000) {
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
} else {
|
||||
return `${(ms / 60000).toFixed(1)}m`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行接口
|
||||
if (require.main === module) {
|
||||
const analyzer = new TestTrendAnalyzer();
|
||||
const command = process.argv[2];
|
||||
|
||||
switch (command) {
|
||||
case 'add':
|
||||
const resultsFile = process.argv[3];
|
||||
if (resultsFile && fs.existsSync(resultsFile)) {
|
||||
const testResults = JSON.parse(fs.readFileSync(resultsFile, 'utf-8'));
|
||||
analyzer.addTestRun(testResults);
|
||||
console.log('✅ 测试数据已添加');
|
||||
} else {
|
||||
console.error('❌ 错误: 请提供有效的测试结果文件');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'report':
|
||||
analyzer.generateTrendReport();
|
||||
break;
|
||||
|
||||
case 'export':
|
||||
const exportFile = process.argv[3] || 'test-trends.json';
|
||||
fs.writeFileSync(exportFile, JSON.stringify(analyzer.trendData, null, 2), 'utf-8');
|
||||
console.log(`✅ 趋势数据已导出到: ${exportFile}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('测试趋势分析工具');
|
||||
console.log('');
|
||||
console.log('用法:');
|
||||
console.log(' node testTrendAnalyzer.js add <results.json> - 添加测试结果');
|
||||
console.log(' node testTrendAnalyzer.js report - 生成趋势报告');
|
||||
console.log(' node testTrendAnalyzer.js export [file.json] - 导出趋势数据');
|
||||
console.log('');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TestTrendAnalyzer;
|
||||
@@ -0,0 +1,348 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
import { TestDataManager } from './utils/testDataManager';
|
||||
import { TestHelper } from './utils/testHelper';
|
||||
|
||||
test.describe('用户管理异常场景测试', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
let userManagementPage: UserManagementPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
userManagementPage = new UserManagementPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
});
|
||||
|
||||
test('创建用户 - 重复用户名', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试创建重复用户名的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const userData = {
|
||||
username: 'admin',
|
||||
nickname: '重复用户',
|
||||
email: 'duplicate@example.com',
|
||||
phone: '13800138000',
|
||||
password: 'Test123!@#',
|
||||
confirmPassword: 'Test123!@#',
|
||||
};
|
||||
|
||||
await userManagementPage.fillUserForm(userData);
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证错误消息', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
|
||||
expect(errorMessage).toContain('用户名已存在');
|
||||
});
|
||||
});
|
||||
|
||||
test('创建用户 - 无效邮箱格式', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试创建无效邮箱的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const userData = {
|
||||
username: `testuser_${Date.now()}`,
|
||||
nickname: '测试用户',
|
||||
email: 'invalid-email',
|
||||
phone: '13800138000',
|
||||
password: 'Test123!@#',
|
||||
confirmPassword: 'Test123!@#',
|
||||
};
|
||||
|
||||
await userManagementPage.fillUserForm(userData);
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证表单验证错误', async () => {
|
||||
const emailInput = page.locator('input[name="email"]');
|
||||
const hasError = await emailInput.evaluate(el => el.classList.contains('is-error'));
|
||||
expect(hasError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('创建用户 - 密码强度不足', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试创建密码强度不足的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const userData = {
|
||||
username: `testuser_${Date.now()}`,
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com',
|
||||
phone: '13800138000',
|
||||
password: '123',
|
||||
confirmPassword: '123',
|
||||
};
|
||||
|
||||
await userManagementPage.fillUserForm(userData);
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证密码强度错误', async () => {
|
||||
const passwordInput = page.locator('input[name="password"]');
|
||||
const hasError = await passwordInput.evaluate(el => el.classList.contains('is-error'));
|
||||
expect(hasError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('创建用户 - 密码不匹配', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试创建密码不匹配的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const userData = {
|
||||
username: `testuser_${Date.now()}`,
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com',
|
||||
phone: '13800138000',
|
||||
password: 'Test123!@#',
|
||||
confirmPassword: 'DifferentPassword',
|
||||
};
|
||||
|
||||
await userManagementPage.fillUserForm(userData);
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证密码不匹配错误', async () => {
|
||||
const confirmPasswordInput = page.locator('input[name="confirmPassword"]');
|
||||
const hasError = await confirmPasswordInput.evaluate(el => el.classList.contains('is-error'));
|
||||
expect(hasError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('创建用户 - 缺少必填字段', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试创建缺少必填字段的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const userData = {
|
||||
username: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
||||
await userManagementPage.fillUserForm(userData);
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证必填字段验证', async () => {
|
||||
const submitButton = page.locator('.el-dialog__footer button[type="submit"]');
|
||||
const isDisabled = await submitButton.evaluate(el => el.disabled);
|
||||
expect(isDisabled).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('创建用户 - 无效手机号格式', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试创建无效手机号的用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const userData = {
|
||||
username: `testuser_${Date.now()}`,
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com',
|
||||
phone: '123',
|
||||
password: 'Test123!@#',
|
||||
confirmPassword: 'Test123!@#',
|
||||
};
|
||||
|
||||
await userManagementPage.fillUserForm(userData);
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证手机号格式错误', async () => {
|
||||
const phoneInput = page.locator('input[name="phone"]');
|
||||
const hasError = await phoneInput.evaluate(el => el.classList.contains('is-error'));
|
||||
expect(hasError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('编辑用户 - 不存在的用户ID', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试编辑不存在的用户', async () => {
|
||||
await page.goto('/users/999999/edit');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('验证404错误或重定向', async () => {
|
||||
const currentUrl = page.url();
|
||||
expect(currentUrl).toMatch(/(404|users)/);
|
||||
});
|
||||
});
|
||||
|
||||
test('删除用户 - 不存在的用户ID', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试删除不存在的用户', async () => {
|
||||
const response = await page.request.delete('http://localhost:8084/api/users/999999');
|
||||
expect(response.status()).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
test('搜索用户 - 空搜索条件', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('执行空搜索', async () => {
|
||||
await userManagementPage.search('');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('验证显示所有用户', async () => {
|
||||
const userCount = await userManagementPage.getUserCount();
|
||||
expect(userCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('搜索用户 - 不存在的用户名', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('搜索不存在的用户', async () => {
|
||||
await userManagementPage.search('nonexistentuser123456');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('验证无结果', async () => {
|
||||
const userCount = await userManagementPage.getUserCount();
|
||||
expect(userCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('批量删除 - 未选择用户', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试批量删除未选择的用户', async () => {
|
||||
await page.click('button:has-text("批量删除")');
|
||||
});
|
||||
|
||||
await test.step('验证提示消息', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
});
|
||||
});
|
||||
|
||||
test('导出用户 - 无数据', async ({ page, request }) => {
|
||||
await test.step('清空用户数据', async () => {
|
||||
const response = await request.delete('http://localhost:8084/api/users/test/cleanup');
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试导出空数据', async () => {
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.click('button:has-text("导出")');
|
||||
const download = await downloadPromise;
|
||||
|
||||
expect(download.suggestedFilename()).toMatch(/users.*\.xlsx/);
|
||||
});
|
||||
});
|
||||
|
||||
test('分页 - 超出范围页码', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('尝试访问超出范围的页码', async () => {
|
||||
await page.goto('/users?page=999999');
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('验证显示最后一页或第一页', async () => {
|
||||
const currentPage = await userManagementPage.getCurrentPage();
|
||||
expect(currentPage).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('网络错误 - 创建用户时断网', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('模拟网络错误', async () => {
|
||||
await page.route('**/api/users', route => route.abort('failed'));
|
||||
});
|
||||
|
||||
await test.step('尝试创建用户', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
|
||||
const userData = {
|
||||
username: `testuser_${Date.now()}`,
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com',
|
||||
phone: '13800138000',
|
||||
password: 'Test123!@#',
|
||||
confirmPassword: 'Test123!@#',
|
||||
};
|
||||
|
||||
await userManagementPage.fillUserForm(userData);
|
||||
await userManagementPage.submitForm();
|
||||
});
|
||||
|
||||
await test.step('验证网络错误提示', async () => {
|
||||
await TestHelper.waitForErrorMessage(page);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,243 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
import { TestDataManager } from './utils/testDataManager';
|
||||
import { TestHelper } from './utils/testHelper';
|
||||
|
||||
test.describe('用户管理 E2E 测试(改进版)', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
let userManagementPage: UserManagementPage;
|
||||
let testUser: any;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
TestDataManager.initialize();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
userManagementPage = new UserManagementPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page, request }) => {
|
||||
await TestHelper.clearAllStorage(page);
|
||||
|
||||
if (testUser) {
|
||||
await TestDataManager.deleteTestUser(request, testUser.username);
|
||||
testUser = null;
|
||||
}
|
||||
});
|
||||
|
||||
test('创建用户完整流程', async ({ page, request }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('点击创建用户按钮', async () => {
|
||||
await userManagementPage.clickCreateUser();
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
});
|
||||
|
||||
await test.step('生成测试用户数据', async () => {
|
||||
testUser = TestDataManager.generateTestUser();
|
||||
console.log('Generated test user:', testUser);
|
||||
});
|
||||
|
||||
await test.step('填写用户表单', async () => {
|
||||
await userManagementPage.fillUserForm(testUser);
|
||||
});
|
||||
|
||||
await test.step('提交表单', async () => {
|
||||
await userManagementPage.submitForm();
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
});
|
||||
|
||||
await test.step('验证用户创建成功', async () => {
|
||||
await TestHelper.waitForTextContent(page, '.el-table', testUser.username);
|
||||
const userCount = await userManagementPage.getUserCount();
|
||||
expect(userCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
await test.step('通过API验证用户存在', async () => {
|
||||
const response = await request.get(`http://localhost:8084/api/users?username=${testUser.username}`);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const result = await response.json();
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test('编辑用户流程', async ({ page, request }) => {
|
||||
await test.step('创建测试用户', async () => {
|
||||
testUser = TestDataManager.generateTestUser();
|
||||
await TestDataManager.createTestUser(request, testUser);
|
||||
});
|
||||
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('搜索并编辑用户', async () => {
|
||||
await userManagementPage.search(testUser.username);
|
||||
await TestHelper.waitForTextContent(page, '.el-table', testUser.username);
|
||||
|
||||
await userManagementPage.editUser(1);
|
||||
await TestHelper.waitForElementVisible(page, '.el-dialog');
|
||||
});
|
||||
|
||||
await test.step('修改用户邮箱', async () => {
|
||||
const newEmail = `updated_${testUser.email}`;
|
||||
await page.fill('input[name="email"]', newEmail);
|
||||
await userManagementPage.submitForm();
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
});
|
||||
|
||||
await test.step('验证修改成功', async () => {
|
||||
await TestHelper.waitForTextContent(page, '.el-table', 'updated_');
|
||||
});
|
||||
});
|
||||
|
||||
test('删除用户流程', async ({ page, request }) => {
|
||||
await test.step('创建测试用户', async () => {
|
||||
testUser = TestDataManager.generateTestUser();
|
||||
await TestDataManager.createTestUser(request, testUser);
|
||||
});
|
||||
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('搜索并删除用户', async () => {
|
||||
await userManagementPage.search(testUser.username);
|
||||
await TestHelper.waitForTextContent(page, '.el-table', testUser.username);
|
||||
|
||||
await userManagementPage.deleteUser(1);
|
||||
await TestHelper.waitForElementVisible(page, '.el-message-box');
|
||||
});
|
||||
|
||||
await test.step('确认删除', async () => {
|
||||
await userManagementPage.confirmDelete();
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
});
|
||||
|
||||
await test.step('验证用户已删除', async () => {
|
||||
await page.reload();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
await userManagementPage.search(testUser.username);
|
||||
|
||||
const userCount = await userManagementPage.getUserCount();
|
||||
expect(userCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('搜索用户功能', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('搜索admin用户', async () => {
|
||||
await userManagementPage.search('admin');
|
||||
await TestHelper.waitForTextContent(page, '.el-table', 'admin');
|
||||
});
|
||||
|
||||
await test.step('验证搜索结果', async () => {
|
||||
const userCount = await userManagementPage.getUserCount();
|
||||
expect(userCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('分页功能', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('获取当前页码', async () => {
|
||||
const currentPage = await userManagementPage.getCurrentPage();
|
||||
expect(currentPage).toBe('1');
|
||||
});
|
||||
|
||||
await test.step('点击下一页', async () => {
|
||||
await userManagementPage.nextPage();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('验证页码变化', async () => {
|
||||
const newPage = await userManagementPage.getCurrentPage();
|
||||
expect(newPage).toBe('2');
|
||||
});
|
||||
});
|
||||
|
||||
test('批量删除用户', async ({ page, request }) => {
|
||||
await test.step('创建多个测试用户', async () => {
|
||||
const users = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const user = TestDataManager.generateTestUser();
|
||||
await TestDataManager.createTestUser(request, user);
|
||||
users.push(user);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('选择多个用户', async () => {
|
||||
await page.check('table tbody tr:nth-child(1) input[type="checkbox"]');
|
||||
await page.check('table tbody tr:nth-child(2) input[type="checkbox"]');
|
||||
});
|
||||
|
||||
await test.step('点击批量删除', async () => {
|
||||
await page.click('button:has-text("批量删除")');
|
||||
await TestHelper.waitForElementVisible(page, '.el-message-box');
|
||||
});
|
||||
|
||||
await test.step('确认删除', async () => {
|
||||
await page.click('.el-message-box__btns .el-button--primary');
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
});
|
||||
});
|
||||
|
||||
test('用户状态切换', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('切换用户状态', async () => {
|
||||
await page.click('table tbody tr:first-child .status-toggle');
|
||||
await TestHelper.waitForSuccessMessage(page);
|
||||
});
|
||||
|
||||
await test.step('验证状态变化', async () => {
|
||||
await page.reload();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
const statusElement = page.locator('table tbody tr:first-child .status-badge');
|
||||
await TestHelper.waitForElementVisible(page, '.status-badge');
|
||||
});
|
||||
});
|
||||
|
||||
test('导出用户数据', async ({ page }) => {
|
||||
await test.step('导航到用户管理页面', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
await TestHelper.waitForPageLoad(page);
|
||||
});
|
||||
|
||||
await test.step('点击导出按钮', async () => {
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.click('button:has-text("导出")');
|
||||
const download = await downloadPromise;
|
||||
|
||||
expect(download.suggestedFilename()).toMatch(/users.*\.xlsx/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,181 @@
|
||||
import { APIRequestContext } from '@playwright/test';
|
||||
|
||||
export interface TestUser {
|
||||
username: string;
|
||||
nickname?: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
password: string;
|
||||
roleIds?: number[];
|
||||
}
|
||||
|
||||
export interface TestRole {
|
||||
roleName: string;
|
||||
roleKey: string;
|
||||
roleSort: string;
|
||||
status: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export class TestDataManager {
|
||||
private static testData: Map<string, any> = new Map();
|
||||
private static apiBaseUrl: string;
|
||||
|
||||
static initialize(apiBaseUrl: string = 'http://localhost:8084') {
|
||||
this.apiBaseUrl = apiBaseUrl;
|
||||
}
|
||||
|
||||
static generateTimestamp(): string {
|
||||
return Date.now().toString();
|
||||
}
|
||||
|
||||
static generateTestUser(override?: Partial<TestUser>): TestUser {
|
||||
const timestamp = this.generateTimestamp();
|
||||
return {
|
||||
username: `testuser_${timestamp}`,
|
||||
nickname: `测试用户${timestamp}`,
|
||||
email: `test_${timestamp}@example.com`,
|
||||
phone: '13800138000',
|
||||
password: 'Test123!@#',
|
||||
roleIds: [],
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
static generateTestRole(override?: Partial<TestRole>): TestRole {
|
||||
const timestamp = this.generateTimestamp();
|
||||
return {
|
||||
roleName: `测试角色_${timestamp}`,
|
||||
roleKey: `test_role_${timestamp}`,
|
||||
roleSort: '1',
|
||||
status: '1',
|
||||
remark: `测试角色备注_${timestamp}`,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
static async createTestUser(request: APIRequestContext, userData: TestUser): Promise<any> {
|
||||
const response = await request.post(`${this.apiBaseUrl}/api/users`, {
|
||||
data: userData,
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to create test user: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const userId = result.data?.id || result.id;
|
||||
|
||||
this.testData.set(`user_${userData.username}`, {
|
||||
id: userId,
|
||||
...userData,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static async createTestRole(request: APIRequestContext, roleData: TestRole): Promise<any> {
|
||||
const response = await request.post(`${this.apiBaseUrl}/api/roles`, {
|
||||
data: roleData,
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to create test role: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const roleId = result.data?.id || result.id;
|
||||
|
||||
this.testData.set(`role_${roleData.roleKey}`, {
|
||||
id: roleId,
|
||||
...roleData,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static async deleteTestUser(request: APIRequestContext, username: string): Promise<void> {
|
||||
const userData = this.testData.get(`user_${username}`);
|
||||
if (!userData || !userData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await request.delete(`${this.apiBaseUrl}/api/users/${userData.id}`);
|
||||
if (!response.ok()) {
|
||||
console.warn(`Failed to delete test user ${username}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
this.testData.delete(`user_${username}`);
|
||||
}
|
||||
|
||||
static async deleteTestRole(request: APIRequestContext, roleKey: string): Promise<void> {
|
||||
const roleData = this.testData.get(`role_${roleKey}`);
|
||||
if (!roleData || !roleData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await request.delete(`${this.apiBaseUrl}/api/roles/${roleData.id}`);
|
||||
if (!response.ok()) {
|
||||
console.warn(`Failed to delete test role ${roleKey}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
this.testData.delete(`role_${roleKey}`);
|
||||
}
|
||||
|
||||
static async cleanupTestData(request: APIRequestContext): Promise<void> {
|
||||
const cleanupPromises: Promise<void>[] = [];
|
||||
|
||||
const entries = Array.from(this.testData.entries());
|
||||
for (const [key, data] of entries) {
|
||||
if (key.startsWith('user_')) {
|
||||
cleanupPromises.push(this.deleteTestUser(request, data.username));
|
||||
} else if (key.startsWith('role_')) {
|
||||
cleanupPromises.push(this.deleteTestRole(request, data.roleKey));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.allSettled(cleanupPromises);
|
||||
this.testData.clear();
|
||||
}
|
||||
|
||||
static getTestData(key: string): any {
|
||||
return this.testData.get(key);
|
||||
}
|
||||
|
||||
static getAllTestData(): Map<string, any> {
|
||||
return new Map(this.testData);
|
||||
}
|
||||
|
||||
static clearTestData(): void {
|
||||
this.testData.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class DatabaseHelper {
|
||||
private static apiBaseUrl: string;
|
||||
|
||||
static initialize(apiBaseUrl: string = 'http://localhost:8084') {
|
||||
this.apiBaseUrl = apiBaseUrl;
|
||||
}
|
||||
|
||||
static async resetDatabase(request: APIRequestContext): Promise<void> {
|
||||
const response = await request.post(`${this.apiBaseUrl}/api/test/reset-database`);
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to reset database: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
static async clearTestData(request: APIRequestContext): Promise<void> {
|
||||
const response = await request.post(`${this.apiBaseUrl}/api/test/clear-test-data`);
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to clear test data: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
static async seedTestData(request: APIRequestContext): Promise<void> {
|
||||
const response = await request.post(`${this.apiBaseUrl}/api/test/seed-test-data`);
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to seed test data: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class TestHelper {
|
||||
static async waitForPageLoad(page: Page, timeout: number = 30000): Promise<void> {
|
||||
await page.waitForLoadState('networkidle', { timeout });
|
||||
await page.waitForLoadState('domcontentloaded', { timeout });
|
||||
}
|
||||
|
||||
static async waitForElementVisible(
|
||||
page: Page,
|
||||
selector: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await expect(page.locator(selector)).toBeVisible({ timeout });
|
||||
}
|
||||
|
||||
static async waitForElementHidden(
|
||||
page: Page,
|
||||
selector: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await expect(page.locator(selector)).toBeHidden({ timeout });
|
||||
}
|
||||
|
||||
static async waitForTextContent(
|
||||
page: Page,
|
||||
selector: string,
|
||||
text: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await expect(page.locator(selector)).toContainText(text, { timeout });
|
||||
}
|
||||
|
||||
static async clickElement(page: Page, selector: string, timeout: number = 10000): Promise<void> {
|
||||
await page.click(selector, { timeout });
|
||||
}
|
||||
|
||||
static async fillInput(
|
||||
page: Page,
|
||||
selector: string,
|
||||
value: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await page.fill(selector, value, { timeout });
|
||||
}
|
||||
|
||||
static async selectOption(
|
||||
page: Page,
|
||||
selector: string,
|
||||
value: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await page.selectOption(selector, value, { timeout });
|
||||
}
|
||||
|
||||
static async checkCheckbox(
|
||||
page: Page,
|
||||
selector: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await page.check(selector, { timeout });
|
||||
}
|
||||
|
||||
static async uncheckCheckbox(
|
||||
page: Page,
|
||||
selector: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await page.uncheck(selector, { timeout });
|
||||
}
|
||||
|
||||
static async uploadFile(
|
||||
page: Page,
|
||||
selector: string,
|
||||
filePath: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await page.setInputFiles(selector, filePath, { timeout });
|
||||
}
|
||||
|
||||
static async takeScreenshot(
|
||||
page: Page,
|
||||
filename: string,
|
||||
fullPage: boolean = false
|
||||
): Promise<void> {
|
||||
await page.screenshot({
|
||||
path: `test-results/screenshots/${filename}`,
|
||||
fullPage,
|
||||
});
|
||||
}
|
||||
|
||||
static async waitForUrl(
|
||||
page: Page,
|
||||
urlPattern: string | RegExp,
|
||||
timeout: number = 30000
|
||||
): Promise<void> {
|
||||
await page.waitForURL(urlPattern, { timeout });
|
||||
}
|
||||
|
||||
static async reloadPage(page: Page, timeout: number = 30000): Promise<void> {
|
||||
await page.reload({ waitUntil: 'networkidle', timeout });
|
||||
}
|
||||
|
||||
static async navigateTo(page: Page, url: string, timeout: number = 30000): Promise<void> {
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout });
|
||||
}
|
||||
|
||||
static async waitForDialog(page: Page, timeout: number = 10000): Promise<void> {
|
||||
await page.waitForEvent('dialog', { timeout });
|
||||
}
|
||||
|
||||
static async handleDialog(page: Page, accept: boolean = true): Promise<void> {
|
||||
page.on('dialog', async (dialog) => {
|
||||
if (accept) {
|
||||
await dialog.accept();
|
||||
} else {
|
||||
await dialog.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async waitForToast(
|
||||
page: Page,
|
||||
message: string,
|
||||
timeout: number = 5000
|
||||
): Promise<void> {
|
||||
await expect(page.locator('.el-message')).toContainText(message, { timeout });
|
||||
}
|
||||
|
||||
static async waitForSuccessMessage(page: Page, timeout: number = 5000): Promise<void> {
|
||||
await expect(page.locator('.el-message--success')).toBeVisible({ timeout });
|
||||
}
|
||||
|
||||
static async waitForErrorMessage(page: Page, timeout: number = 5000): Promise<void> {
|
||||
await expect(page.locator('.el-message--error')).toBeVisible({ timeout });
|
||||
}
|
||||
|
||||
static async getElementText(page: Page, selector: string): Promise<string> {
|
||||
const text = await page.textContent(selector);
|
||||
return text || '';
|
||||
}
|
||||
|
||||
static async getElementCount(page: Page, selector: string): Promise<number> {
|
||||
return await page.locator(selector).count();
|
||||
}
|
||||
|
||||
static async isElementVisible(page: Page, selector: string): Promise<boolean> {
|
||||
return await page.locator(selector).isVisible();
|
||||
}
|
||||
|
||||
static async isElementEnabled(page: Page, selector: string): Promise<boolean> {
|
||||
return await page.locator(selector).isEnabled();
|
||||
}
|
||||
|
||||
static async scrollToElement(page: Page, selector: string): Promise<void> {
|
||||
await page.locator(selector).scrollIntoViewIfNeeded();
|
||||
}
|
||||
|
||||
static async hoverElement(page: Page, selector: string): Promise<void> {
|
||||
await page.hover(selector);
|
||||
}
|
||||
|
||||
static async doubleClickElement(page: Page, selector: string): Promise<void> {
|
||||
await page.dblclick(selector);
|
||||
}
|
||||
|
||||
static async rightClickElement(page: Page, selector: string): Promise<void> {
|
||||
await page.click(selector, { button: 'right' });
|
||||
}
|
||||
|
||||
static async waitForApiResponse(
|
||||
page: Page,
|
||||
urlPattern: string | RegExp,
|
||||
timeout: number = 30000
|
||||
): Promise<void> {
|
||||
await page.waitForResponse(
|
||||
(response) => !!response.url().match(urlPattern),
|
||||
{ timeout }
|
||||
);
|
||||
}
|
||||
|
||||
static async getApiResponse(
|
||||
page: Page,
|
||||
urlPattern: string | RegExp,
|
||||
timeout: number = 30000
|
||||
): Promise<any> {
|
||||
const response = await page.waitForResponse(
|
||||
(response) => !!response.url().match(urlPattern),
|
||||
{ timeout }
|
||||
);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
static async mockApiResponse(
|
||||
page: Page,
|
||||
urlPattern: string | RegExp,
|
||||
mockData: any
|
||||
): Promise<void> {
|
||||
await page.route(urlPattern, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockData),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static async executeScript(page: Page, script: string): Promise<any> {
|
||||
return await page.evaluate(script);
|
||||
}
|
||||
|
||||
static async setLocalStorage(page: Page, key: string, value: string): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ key, value }) => {
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
{ key, value }
|
||||
);
|
||||
}
|
||||
|
||||
static async getLocalStorage(page: Page, key: string): Promise<string | null> {
|
||||
return await page.evaluate((key) => localStorage.getItem(key), key);
|
||||
}
|
||||
|
||||
static async clearLocalStorage(page: Page): Promise<void> {
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
}
|
||||
|
||||
static async setSessionStorage(page: Page, key: string, value: string): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ key, value }) => {
|
||||
sessionStorage.setItem(key, value);
|
||||
},
|
||||
{ key, value }
|
||||
);
|
||||
}
|
||||
|
||||
static async clearSessionStorage(page: Page): Promise<void> {
|
||||
await page.evaluate(() => sessionStorage.clear());
|
||||
}
|
||||
|
||||
static async clearCookies(page: Page): Promise<void> {
|
||||
await page.context().clearCookies();
|
||||
}
|
||||
|
||||
static async clearAllStorage(page: Page): Promise<void> {
|
||||
await this.clearLocalStorage(page);
|
||||
await this.clearSessionStorage(page);
|
||||
await this.clearCookies(page);
|
||||
}
|
||||
|
||||
static async getAuthToken(page: Page): Promise<string> {
|
||||
const token = await this.getLocalStorage(page, 'token');
|
||||
if (!token) {
|
||||
const user = await this.getLocalStorage(page, 'user');
|
||||
if (user) {
|
||||
const userData = JSON.parse(user);
|
||||
return userData.token || '';
|
||||
}
|
||||
}
|
||||
return token || '';
|
||||
}
|
||||
}
|
||||
Generated
+238
-62
@@ -23,6 +23,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@vitest/coverage-v8": "^4.1.1",
|
||||
"@vitest/ui": "^4.0.16",
|
||||
"@vue/test-utils": "^2.4.3",
|
||||
"eslint": "^8.56.0",
|
||||
@@ -123,6 +124,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
|
||||
@@ -998,12 +1009,33 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -2089,18 +2121,49 @@
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
|
||||
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.1.tgz",
|
||||
"integrity": "sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"@vitest/utils": "4.1.1",
|
||||
"ast-v8-to-istanbul": "^1.0.0",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.2.0",
|
||||
"magicast": "^0.5.2",
|
||||
"obug": "^2.1.1",
|
||||
"std-env": "^4.0.0-rc.1",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "4.1.1",
|
||||
"vitest": "4.1.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz",
|
||||
"integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.0.18",
|
||||
"@vitest/utils": "4.0.18",
|
||||
"chai": "^6.2.1",
|
||||
"@vitest/spy": "4.1.1",
|
||||
"@vitest/utils": "4.1.1",
|
||||
"chai": "^6.2.2",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -2108,13 +2171,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
|
||||
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz",
|
||||
"integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.0.18",
|
||||
"@vitest/spy": "4.1.1",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
@@ -2123,7 +2186,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0-0"
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
@@ -2135,9 +2198,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
||||
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz",
|
||||
"integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2148,13 +2211,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
|
||||
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz",
|
||||
"integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.18",
|
||||
"@vitest/utils": "4.1.1",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -2162,13 +2225,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
||||
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz",
|
||||
"integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"@vitest/pretty-format": "4.1.1",
|
||||
"@vitest/utils": "4.1.1",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -2177,9 +2241,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
|
||||
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz",
|
||||
"integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -2187,15 +2251,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/ui": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.18.tgz",
|
||||
"integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.1.tgz",
|
||||
"integrity": "sha512-k0qNVLmCISxoGWvdhOeynlZVrfjx7Xjp95kIptN0fZYyONCgVcKIPn53MpFZ7S+fO6YdKNhgIfl0nu92Q0CCOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.18",
|
||||
"@vitest/utils": "4.1.1",
|
||||
"fflate": "^0.8.2",
|
||||
"flatted": "^3.3.3",
|
||||
"flatted": "3.4.0",
|
||||
"pathe": "^2.0.3",
|
||||
"sirv": "^3.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
@@ -2205,17 +2269,25 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "4.0.18"
|
||||
"vitest": "4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/ui/node_modules/flatted": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.0.tgz",
|
||||
"integrity": "sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
|
||||
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz",
|
||||
"integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"@vitest/pretty-format": "4.1.1",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -2604,6 +2676,18 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz",
|
||||
"integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"estree-walker": "^3.0.3",
|
||||
"js-tokens": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
@@ -2820,6 +2904,13 @@
|
||||
"proto-list": "~1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/copy-anything": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||
@@ -3121,9 +3212,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
||||
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3856,6 +3947,13 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
@@ -4035,6 +4133,45 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
@@ -4083,6 +4220,13 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
@@ -4256,6 +4400,34 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz",
|
||||
"integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -5546,9 +5718,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
|
||||
"integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -6089,31 +6261,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz",
|
||||
"integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"@vitest/runner": "4.0.18",
|
||||
"@vitest/snapshot": "4.0.18",
|
||||
"@vitest/spy": "4.0.18",
|
||||
"@vitest/utils": "4.0.18",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"expect-type": "^1.2.2",
|
||||
"@vitest/expect": "4.1.1",
|
||||
"@vitest/mocker": "4.1.1",
|
||||
"@vitest/pretty-format": "4.1.1",
|
||||
"@vitest/runner": "4.1.1",
|
||||
"@vitest/snapshot": "4.1.1",
|
||||
"@vitest/spy": "4.1.1",
|
||||
"@vitest/utils": "4.1.1",
|
||||
"es-module-lexer": "^2.0.0",
|
||||
"expect-type": "^1.3.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^3.10.0",
|
||||
"std-env": "^4.0.0-rc.1",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.0.3",
|
||||
"vite": "^6.0.0 || ^7.0.0",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -6129,12 +6301,13 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.0.18",
|
||||
"@vitest/browser-preview": "4.0.18",
|
||||
"@vitest/browser-webdriverio": "4.0.18",
|
||||
"@vitest/ui": "4.0.18",
|
||||
"@vitest/browser-playwright": "4.1.1",
|
||||
"@vitest/browser-preview": "4.1.1",
|
||||
"@vitest/browser-webdriverio": "4.1.1",
|
||||
"@vitest/ui": "4.1.1",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
"jsdom": "*",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
@@ -6163,6 +6336,9 @@
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,11 +13,19 @@
|
||||
"preview": "vite preview",
|
||||
"test": "vitest --run",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:unit": "vitest --run --coverage",
|
||||
"test:coverage": "vitest --run --coverage",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:perf": "node scripts/measure-e2e-performance.js",
|
||||
"test:perf": "node scripts/performance-test.js performance",
|
||||
"test:load": "node scripts/performance-test.js load",
|
||||
"test:perf:all": "node scripts/performance-test.js all",
|
||||
"test:edge": "playwright test edge-cases.spec.ts",
|
||||
"test:performance-opt": "playwright test performance-optimization.spec.ts",
|
||||
"test:parallel-opt": "playwright test parallel-optimization.spec.ts",
|
||||
"test:all-opt": "playwright test edge-cases.spec.ts performance-optimization.spec.ts parallel-optimization.spec.ts",
|
||||
"test:monitor": "node e2e/performanceMonitor.js report",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
@@ -37,6 +45,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@vitest/coverage-v8": "^4.1.1",
|
||||
"@vitest/ui": "^4.0.16",
|
||||
"@vue/test-utils": "^2.4.3",
|
||||
"eslint": "^8.56.0",
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const isHeadless = process.env.PLAYWRIGHT_HEADLESS === 'true' || process.env.CI === 'true';
|
||||
const baseURL = process.env.TEST_BASE_URL || process.env.VITE_BASE_URL || 'http://localhost:3001';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 1,
|
||||
workers: process.env.CI ? 2 : 4,
|
||||
retries: 2,
|
||||
workers: process.env.CI ? 4 : 6,
|
||||
reporter: [
|
||||
['html', { outputFolder: 'playwright-report' }],
|
||||
['json', { outputFile: 'test-results/results.json' }],
|
||||
['junit', { outputFile: 'test-results/junit.xml' }],
|
||||
['list']
|
||||
['list'],
|
||||
['./e2e/customReporter.ts']
|
||||
],
|
||||
|
||||
timeout: 60000,
|
||||
timeout: 90000,
|
||||
expect: {
|
||||
timeout: 10000
|
||||
timeout: 20000
|
||||
},
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:3001',
|
||||
trace: 'retain-on-failure',
|
||||
baseURL: baseURL,
|
||||
trace: process.env.CI ? 'retain-on-failure' : 'off',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
actionTimeout: 15000,
|
||||
navigationTimeout: 30000,
|
||||
video: process.env.CI ? 'retain-on-failure' : 'off',
|
||||
actionTimeout: 30000,
|
||||
navigationTimeout: 60000,
|
||||
headless: isHeadless,
|
||||
locale: 'zh-CN',
|
||||
timezoneId: 'Asia/Shanghai',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import request from '@/utils/request'
|
||||
import type { PageResponse } from './user.api'
|
||||
import { RoleStatus } from '@/constants/status'
|
||||
|
||||
export interface Role {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
description: string
|
||||
status: 'ACTIVE' | 'INACTIVE'
|
||||
roleName: string
|
||||
roleKey: string
|
||||
roleSort: number
|
||||
status: RoleStatus
|
||||
permissions: Permission[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
@@ -21,24 +22,25 @@ export interface Permission {
|
||||
}
|
||||
|
||||
export interface CreateRoleRequest {
|
||||
name: string
|
||||
code: string
|
||||
description: string
|
||||
roleName: string
|
||||
roleKey: string
|
||||
roleSort: number
|
||||
permissions: number[]
|
||||
}
|
||||
|
||||
export interface UpdateRoleRequest {
|
||||
name?: string
|
||||
description?: string
|
||||
status?: 'ACTIVE' | 'INACTIVE'
|
||||
roleName?: string
|
||||
roleKey?: string
|
||||
roleSort?: number
|
||||
status?: RoleStatus
|
||||
permissions?: number[]
|
||||
}
|
||||
|
||||
export interface RolePageRequest {
|
||||
page: number
|
||||
size: number
|
||||
name?: string
|
||||
code?: string
|
||||
roleName?: string
|
||||
roleKey?: string
|
||||
status?: string
|
||||
sortBy?: string
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
import { UserStatus } from '@/constants/status'
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
@@ -7,7 +8,7 @@ export interface User {
|
||||
email: string
|
||||
phone: string
|
||||
avatar: string
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'LOCKED'
|
||||
status: UserStatus
|
||||
roles: string[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
@@ -27,7 +28,7 @@ export interface UpdateUserRequest {
|
||||
email?: string
|
||||
phone?: string
|
||||
avatar?: string
|
||||
status?: 'ACTIVE' | 'INACTIVE' | 'LOCKED'
|
||||
status?: UserStatus
|
||||
roles?: string[]
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ export const userApi = {
|
||||
resetPassword: (id: number) =>
|
||||
request.post<void>(`/users/${id}/reset-password`),
|
||||
|
||||
updateStatus: (id: number, status: 'ACTIVE' | 'INACTIVE' | 'LOCKED') =>
|
||||
updateStatus: (id: number, status: UserStatus) =>
|
||||
request.put<void>(`/users/${id}/status`, { status }),
|
||||
|
||||
assignRoles: (id: number, roleIds: number[]) =>
|
||||
|
||||
@@ -32,3 +32,61 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.el-message {
|
||||
--el-message-bg-color: var(--el-color-success-dark-2);
|
||||
--el-message-border-color: var(--el-color-success-dark-2);
|
||||
--el-message-text-color: #ffffff;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-message--success {
|
||||
--el-message-bg-color: var(--el-color-success-dark-2);
|
||||
--el-message-border-color: var(--el-color-success-dark-2);
|
||||
--el-message-text-color: #ffffff;
|
||||
}
|
||||
|
||||
.el-message--error {
|
||||
--el-message-bg-color: var(--el-color-danger-dark-2);
|
||||
--el-message-border-color: var(--el-color-danger-dark-2);
|
||||
--el-message-text-color: #ffffff;
|
||||
}
|
||||
|
||||
.el-message--warning {
|
||||
--el-message-bg-color: var(--el-color-warning-dark-2);
|
||||
--el-message-border-color: var(--el-color-warning-dark-2);
|
||||
--el-message-text-color: #ffffff;
|
||||
}
|
||||
|
||||
.el-message--info {
|
||||
--el-message-bg-color: var(--el-color-info-dark-2);
|
||||
--el-message-border-color: var(--el-color-info-dark-2);
|
||||
--el-message-text-color: #ffffff;
|
||||
}
|
||||
|
||||
.el-tag.el-tag--light {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--el-color-danger-light-9);
|
||||
border-color: var(--el-color-danger-light-9);
|
||||
}
|
||||
|
||||
.el-tag.el-tag--light.el-tag--success {
|
||||
background-color: var(--el-color-success-light-9);
|
||||
border-color: var(--el-color-success-light-9);
|
||||
}
|
||||
|
||||
.el-tag.el-tag--light.el-tag--warning {
|
||||
background-color: var(--el-color-warning-light-9);
|
||||
border-color: var(--el-color-warning-light-9);
|
||||
}
|
||||
|
||||
.el-tag.el-tag--light.el-tag--info {
|
||||
background-color: var(--el-color-info-light-9);
|
||||
border-color: var(--el-color-info-light-9);
|
||||
}
|
||||
|
||||
.el-tag.el-tag--light.el-tag--danger {
|
||||
background-color: var(--el-color-danger-light-9);
|
||||
border-color: var(--el-color-danger-light-9);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 系统状态值常量定义
|
||||
*
|
||||
* 统一前后端状态值,避免不一致导致的功能问题
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-24
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户状态枚举
|
||||
*/
|
||||
export enum UserStatus {
|
||||
/** 正常 */
|
||||
ACTIVE = 1,
|
||||
/** 禁用 */
|
||||
INACTIVE = 0,
|
||||
/** 锁定 */
|
||||
LOCKED = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色状态枚举
|
||||
*/
|
||||
export enum RoleStatus {
|
||||
/** 正常 */
|
||||
ACTIVE = 1,
|
||||
/** 禁用 */
|
||||
INACTIVE = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单状态枚举
|
||||
*/
|
||||
export enum MenuStatus {
|
||||
/** 正常 */
|
||||
ACTIVE = 1,
|
||||
/** 禁用 */
|
||||
INACTIVE = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知状态枚举
|
||||
*/
|
||||
export enum NoticeStatus {
|
||||
/** 正常 */
|
||||
ACTIVE = '1',
|
||||
/** 禁用 */
|
||||
INACTIVE = '0'
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态值映射工具类
|
||||
*/
|
||||
export class StatusHelper {
|
||||
/**
|
||||
* 判断状态是否为正常
|
||||
*/
|
||||
static isActive(status: number | string): boolean {
|
||||
return status === 1 || status === '1' || status === 'ACTIVE'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断状态是否为禁用
|
||||
*/
|
||||
static isInactive(status: number | string): boolean {
|
||||
return status === 0 || status === '0' || status === 'INACTIVE'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态显示文本
|
||||
*/
|
||||
static getStatusText(status: number | string): string {
|
||||
if (this.isActive(status)) return '正常'
|
||||
if (this.isInactive(status)) return '禁用'
|
||||
return '未知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态标签类型
|
||||
*/
|
||||
static getStatusType(status: number | string): 'success' | 'danger' | 'warning' {
|
||||
if (this.isActive(status)) return 'success'
|
||||
if (this.isInactive(status)) return 'danger'
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import ConfigManagement from '@/views/config/ConfigManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/request', () => {
|
||||
const mockRequest = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}
|
||||
|
||||
mockRequest.get.mockResolvedValue([])
|
||||
mockRequest.post.mockResolvedValue({})
|
||||
mockRequest.put.mockResolvedValue({})
|
||||
mockRequest.delete.mockResolvedValue({})
|
||||
|
||||
return {
|
||||
default: mockRequest,
|
||||
}
|
||||
})
|
||||
|
||||
describe('ConfigManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render config management container', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.config-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty data source', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.dataSource).toBeDefined()
|
||||
expect(Array.isArray(wrapper.vm.dataSource)).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with loading state false', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBeDefined()
|
||||
expect(typeof wrapper.vm.loading).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should initialize with modal visible false', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.configName).toBe('')
|
||||
expect(wrapper.vm.formState.configKey).toBe('')
|
||||
expect(wrapper.vm.formState.configValue).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add config functionality', () => {
|
||||
it('should have handleAdd method', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edit config functionality', () => {
|
||||
it('should have handleEdit method', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete config functionality', () => {
|
||||
it('should have handleDelete method', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('form submission', () => {
|
||||
it('should have handleModalOk method', () => {
|
||||
wrapper = mount(ConfigManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleModalOk).toBe('function')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,261 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import Dashboard from '@/views/system/Dashboard.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('@/api/user.api.ts', () => ({
|
||||
getUserStats: vi.fn(),
|
||||
getRecentLogins: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('Dashboard Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Dashboard</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render dashboard container', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.dashboard').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with loading state', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty stats', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.stats).toEqual({
|
||||
userCount: 0,
|
||||
roleCount: 0,
|
||||
todayLogin: 0,
|
||||
operationLog: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('statistics cards', () => {
|
||||
it('should render user count card', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.stats.userCount).toBeDefined()
|
||||
})
|
||||
|
||||
it('should render role count card', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.stats.roleCount).toBeDefined()
|
||||
})
|
||||
|
||||
it('should render today login card', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.stats.todayLogin).toBeDefined()
|
||||
})
|
||||
|
||||
it('should render operation log card', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.stats.operationLog).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('recent logins', () => {
|
||||
it('should initialize with empty recent logins', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.recentLogins).toEqual([])
|
||||
})
|
||||
|
||||
it('should display empty state when no recent logins', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.recentLogins.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('data loading', () => {
|
||||
it('should set loading to false after data loaded', async () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBe(true)
|
||||
|
||||
wrapper.vm.loading = false
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('document title', () => {
|
||||
it('should have dashboard component mounted', () => {
|
||||
wrapper = mount(Dashboard, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-row': true,
|
||||
'el-col': true,
|
||||
'el-card': true,
|
||||
'el-statistic': true,
|
||||
'el-icon': true,
|
||||
'el-timeline': true,
|
||||
'el-timeline-item': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,286 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import DictManagement from '@/views/config/DictManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/request', () => {
|
||||
const mockRequest = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}
|
||||
|
||||
mockRequest.get.mockResolvedValue([])
|
||||
mockRequest.post.mockResolvedValue({})
|
||||
mockRequest.put.mockResolvedValue({})
|
||||
mockRequest.delete.mockResolvedValue({})
|
||||
|
||||
return {
|
||||
default: mockRequest,
|
||||
}
|
||||
})
|
||||
|
||||
describe('DictManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render dict management container', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.dict-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty data source', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.dataSource).toBeDefined()
|
||||
expect(Array.isArray(wrapper.vm.dataSource)).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with loading state false', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBeDefined()
|
||||
expect(typeof wrapper.vm.loading).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should initialize with modal visible false', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.dictName).toBe('')
|
||||
expect(wrapper.vm.formState.dictType).toBe('')
|
||||
expect(wrapper.vm.formState.status).toBe('0')
|
||||
expect(wrapper.vm.formState.remark).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add dict functionality', () => {
|
||||
it('should have handleAdd method', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edit dict functionality', () => {
|
||||
it('should have handleEdit method', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete dict functionality', () => {
|
||||
it('should have handleDelete method', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('form submission', () => {
|
||||
it('should have handleModalOk method', () => {
|
||||
wrapper = mount(DictManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleModalOk).toBe('function')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,181 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import Login from '@/views/system/Login.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/request', () => ({
|
||||
default: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('Login Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Dashboard</div>' } },
|
||||
{ path: '/login', component: { template: '<div>Login</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component rendering', () => {
|
||||
it('should render login form', () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.login-container').exists()).toBe(true)
|
||||
expect(wrapper.find('.login-card').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.username).toBe('')
|
||||
expect(wrapper.vm.formState.password).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize loading as false', () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('form state management', () => {
|
||||
it('should update username when input changes', async () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.vm.formState.username = 'testuser'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.formState.username).toBe('testuser')
|
||||
})
|
||||
|
||||
it('should update password when input changes', async () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.vm.formState.password = 'password123'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.formState.password).toBe('password123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('form submission', () => {
|
||||
it('should have onFinish method', () => {
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.onFinish).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('document title', () => {
|
||||
it('should set document title on mount', () => {
|
||||
const originalTitle = document.title
|
||||
|
||||
wrapper = mount(Login, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(document.title).toBe('登录 - Novalon 管理系统')
|
||||
|
||||
document.title = originalTitle
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,279 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import MenuManagement from '@/views/system/MenuManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/menu.api', () => ({
|
||||
menuApi: {
|
||||
getAll: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/request', () => {
|
||||
const mockRequest = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}
|
||||
|
||||
mockRequest.get.mockResolvedValue([])
|
||||
mockRequest.post.mockResolvedValue({})
|
||||
mockRequest.put.mockResolvedValue({})
|
||||
mockRequest.delete.mockResolvedValue({})
|
||||
|
||||
return {
|
||||
default: mockRequest,
|
||||
}
|
||||
})
|
||||
|
||||
describe('MenuManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render menu management container', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.menu-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty data source', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.dataSource).toBeDefined()
|
||||
expect(Array.isArray(wrapper.vm.dataSource)).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with loading state false', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBeDefined()
|
||||
expect(typeof wrapper.vm.loading).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should initialize with modal visible false', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.menuName).toBe('')
|
||||
expect(wrapper.vm.formState.menuType).toBe('C')
|
||||
expect(wrapper.vm.formState.perms).toBe('')
|
||||
expect(wrapper.vm.formState.component).toBe('')
|
||||
expect(wrapper.vm.formState.orderNum).toBe(0)
|
||||
expect(wrapper.vm.formState.status).toBe('0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add menu functionality', () => {
|
||||
it('should have handleAdd method', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edit menu functionality', () => {
|
||||
it('should have handleEdit method', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleEdit).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete menu functionality', () => {
|
||||
it('should have handleDelete method', () => {
|
||||
wrapper = mount(MenuManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-input': true,
|
||||
'el-input-number': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleDelete).toBe('function')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,383 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import RoleManagement from '@/views/system/RoleManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/role.api', () => ({
|
||||
roleApi: {
|
||||
getPage: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/permission.api', () => ({
|
||||
permissionApi: {
|
||||
getAll: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('RoleManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render role management container', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.role-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty search keyword', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize with empty data source', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.dataSource).toEqual([])
|
||||
})
|
||||
|
||||
it('should initialize with pagination on page 1', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
})
|
||||
|
||||
it('should initialize with modal visible false', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.roleName).toBe('')
|
||||
expect(wrapper.vm.formState.roleKey).toBe('')
|
||||
expect(wrapper.vm.formState.roleSort).toBe(1)
|
||||
expect(wrapper.vm.formState.status).toBe(1)
|
||||
expect(wrapper.vm.formState.permissions).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('search functionality', () => {
|
||||
it('should have handleSearch method', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSearch).toBe('function')
|
||||
})
|
||||
|
||||
it('should update search keyword when input changes', async () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.vm.searchKeyword = 'admin'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('admin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add role functionality', () => {
|
||||
it('should have handleAdd method', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pagination functionality', () => {
|
||||
it('should have handleTableChange method', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleTableChange).toBe('function')
|
||||
})
|
||||
|
||||
it('should have handleSizeChange method', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSizeChange).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sort functionality', () => {
|
||||
it('should have handleSortChange method', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSortChange).toBe('function')
|
||||
})
|
||||
|
||||
it('should initialize with default sort info', () => {
|
||||
wrapper = mount(RoleManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-tree': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.sortInfo.sortBy).toBe('id')
|
||||
expect(wrapper.vm.sortInfo.sortOrder).toBe('asc')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,423 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import UserManagement from '@/views/system/UserManagement.vue'
|
||||
|
||||
vi.mock('vue-router')
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
ElMessageBox: {
|
||||
confirm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/user.api', () => ({
|
||||
userApi: {
|
||||
getPage: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
assignRoles: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/role.api', () => ({
|
||||
roleApi: {
|
||||
getAll: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('UserManagement Component', () => {
|
||||
let router: any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
],
|
||||
})
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('component initialization', () => {
|
||||
it('should render user management container', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.user-management').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty search keyword', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize with loading state false before data fetch', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.loading).toBeDefined()
|
||||
expect(typeof wrapper.vm.loading).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should initialize with empty data source', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.dataSource).toEqual([])
|
||||
})
|
||||
|
||||
it('should initialize with pagination on page 1', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.pagination.current).toBe(1)
|
||||
})
|
||||
|
||||
it('should initialize with modal visible false', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.modalVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize with empty form state', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.formState.username).toBe('')
|
||||
expect(wrapper.vm.formState.password).toBe('')
|
||||
expect(wrapper.vm.formState.nickname).toBe('')
|
||||
expect(wrapper.vm.formState.email).toBe('')
|
||||
expect(wrapper.vm.formState.phone).toBe('')
|
||||
expect(wrapper.vm.formState.roles).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('search functionality', () => {
|
||||
it('should have handleSearch method', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSearch).toBe('function')
|
||||
})
|
||||
|
||||
it('should update search keyword when input changes', async () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.vm.searchKeyword = 'testuser'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.searchKeyword).toBe('testuser')
|
||||
})
|
||||
})
|
||||
|
||||
describe('add user functionality', () => {
|
||||
it('should have handleAdd method', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleAdd).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pagination functionality', () => {
|
||||
it('should have handleTableChange method', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleTableChange).toBe('function')
|
||||
})
|
||||
|
||||
it('should have handleSizeChange method', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSizeChange).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sort functionality', () => {
|
||||
it('should have handleSortChange method', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof wrapper.vm.handleSortChange).toBe('function')
|
||||
})
|
||||
|
||||
it('should initialize with default sort info', () => {
|
||||
wrapper = mount(UserManagement, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
'el-card': true,
|
||||
'el-input': true,
|
||||
'el-button': true,
|
||||
'el-table': true,
|
||||
'el-table-column': true,
|
||||
'el-tag': true,
|
||||
'el-pagination': true,
|
||||
'el-dialog': true,
|
||||
'el-form': true,
|
||||
'el-form-item': true,
|
||||
'el-select': true,
|
||||
'el-option': true,
|
||||
'el-icon': true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.sortInfo.sortBy).toBe('id')
|
||||
expect(wrapper.vm.sortInfo.sortOrder).toBe('asc')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
describe('Vitest Configuration Test', () => {
|
||||
it('should run a simple test', () => {
|
||||
expect(1 + 1).toBe(2)
|
||||
})
|
||||
|
||||
it('should handle async operations', async () => {
|
||||
const result = await Promise.resolve(42)
|
||||
expect(result).toBe(42)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
export const mockUser = {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
nickname: 'Test User',
|
||||
email: 'test@example.com',
|
||||
phone: '13800138000',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
roles: ['admin'],
|
||||
permissions: ['user:view', 'user:create', 'user:edit', 'user:delete'],
|
||||
}
|
||||
|
||||
export const mockRole = {
|
||||
id: 1,
|
||||
roleName: '测试角色',
|
||||
roleKey: 'test_role',
|
||||
roleSort: 1,
|
||||
status: '1',
|
||||
remark: '测试角色备注',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const mockMenu = {
|
||||
id: 1,
|
||||
menuName: '系统管理',
|
||||
parentId: 0,
|
||||
orderNum: 1,
|
||||
menuType: 'M',
|
||||
component: 'system',
|
||||
perms: 'system:view',
|
||||
status: '1',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const mockDict = {
|
||||
id: 1,
|
||||
dictName: '用户状态',
|
||||
dictType: 'user_status',
|
||||
status: '1',
|
||||
remark: '用户状态字典',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const mockConfig = {
|
||||
id: 1,
|
||||
configName: '系统名称',
|
||||
configKey: 'sys.name',
|
||||
configValue: 'Novalon管理系统',
|
||||
configType: 'Y',
|
||||
status: '1',
|
||||
remark: '系统名称配置',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const mockNotice = {
|
||||
id: 1,
|
||||
noticeTitle: '系统通知',
|
||||
noticeType: '1',
|
||||
noticeContent: '这是一条测试通知',
|
||||
status: '0',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
}
|
||||
|
||||
export const mockLoginRequest = {
|
||||
username: 'admin',
|
||||
password: 'admin123',
|
||||
}
|
||||
|
||||
export const mockLoginResponse = {
|
||||
token: 'mock-jwt-token',
|
||||
user: mockUser,
|
||||
}
|
||||
|
||||
export const mockApiResponse = <T>(data: T, code = 200, message = 'success') => ({
|
||||
code,
|
||||
message,
|
||||
data,
|
||||
})
|
||||
|
||||
export const mockErrorResponse = (code = 500, message = 'Internal Server Error') => ({
|
||||
code,
|
||||
message,
|
||||
data: null,
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import { vi } from 'vitest'
|
||||
import { config } from '@vue/test-utils'
|
||||
|
||||
config.global.stubs = {
|
||||
transition: false,
|
||||
'transition-group': false,
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
})
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
},
|
||||
})
|
||||
|
||||
Object.defineProperty(window, 'sessionStorage', {
|
||||
value: {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { VueWrapper } from '@vue/test-utils'
|
||||
import { ComponentPublicInstance } from 'vue'
|
||||
|
||||
export interface TestHelpers {
|
||||
findByText: (text: string) => HTMLElement | null
|
||||
findByTestId: (testId: string) => HTMLElement | null
|
||||
clickByText: (text: string) => Promise<void>
|
||||
clickByTestId: (testId: string) => Promise<void>
|
||||
fillByTestId: (testId: string, value: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function createTestHelpers(wrapper: VueWrapper<ComponentPublicInstance>): TestHelpers {
|
||||
return {
|
||||
findByText: (text: string) => {
|
||||
return wrapper.element.textContent?.includes(text) ? wrapper.element : null
|
||||
},
|
||||
findByTestId: (testId: string) => {
|
||||
return wrapper.element.querySelector(`[data-testid="${testId}"]`)
|
||||
},
|
||||
clickByText: async (text: string) => {
|
||||
const element = wrapper.element.textContent?.includes(text) ? wrapper.element : null
|
||||
if (element) {
|
||||
element.click()
|
||||
await wrapper.vm.$nextTick()
|
||||
}
|
||||
},
|
||||
clickByTestId: async (testId: string) => {
|
||||
const element = wrapper.element.querySelector(`[data-testid="${testId}"]`)
|
||||
if (element) {
|
||||
element.click()
|
||||
await wrapper.vm.$nextTick()
|
||||
}
|
||||
},
|
||||
fillByTestId: async (testId: string, value: string) => {
|
||||
const element = wrapper.element.querySelector(`[data-testid="${testId}"]`) as HTMLInputElement
|
||||
if (element) {
|
||||
element.value = value
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await wrapper.vm.$nextTick()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function waitFor(condition: () => boolean, timeout = 5000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now()
|
||||
|
||||
const check = () => {
|
||||
if (condition()) {
|
||||
resolve()
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
reject(new Error(`Timeout waiting for condition`))
|
||||
} else {
|
||||
setTimeout(check, 100)
|
||||
}
|
||||
}
|
||||
|
||||
check()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { handleApiError, ApiErrorHandler } from '@/utils/errorHandler'
|
||||
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('errorHandler', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal('localStorage', {
|
||||
removeItem: vi.fn(),
|
||||
})
|
||||
vi.stubGlobal('window', {
|
||||
location: { href: '' },
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleApiError', () => {
|
||||
it('should call ApiErrorHandler.handle', () => {
|
||||
const mockError = { response: { status: 500, data: {} } }
|
||||
const handleSpy = vi.spyOn(ApiErrorHandler, 'handle')
|
||||
|
||||
handleApiError(mockError)
|
||||
|
||||
expect(handleSpy).toHaveBeenCalledWith(mockError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ApiErrorHandler.handle', () => {
|
||||
it('should handle network error', () => {
|
||||
const mockError = new Error('Network Error')
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('网络连接失败,请检查网络设置')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Network Error:', mockError)
|
||||
})
|
||||
|
||||
it('should handle 400 Bad Request', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 400,
|
||||
data: { message: 'Invalid parameters' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('Invalid parameters')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Bad Request:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 401 Unauthorized', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 401,
|
||||
data: { message: 'Unauthorized' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('登录已过期,请重新登录')
|
||||
expect(localStorage.removeItem).toHaveBeenCalledWith('token')
|
||||
expect(window.location.href).toBe('/login')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Unauthorized:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 403 Forbidden', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 403,
|
||||
data: { message: 'Access denied' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('没有权限访问该资源')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Forbidden:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 404 Not Found', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 404,
|
||||
data: { message: 'Resource not found' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('Resource not found')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Not Found:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 409 Conflict', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 409,
|
||||
data: { message: 'Resource conflict' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('Resource conflict')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Conflict:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 422 Validation Error with details', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 422,
|
||||
data: {
|
||||
message: 'Validation failed',
|
||||
details: {
|
||||
username: 'Username is required',
|
||||
password: 'Password is too short',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('Username is required、Password is too short')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Validation Error:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 422 Validation Error without details', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 422,
|
||||
data: { message: 'Validation failed' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('Validation failed')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Validation Error:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 500 Internal Server Error', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 500,
|
||||
data: { message: 'Server error' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('服务器内部错误,请稍后重试')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Internal Server Error:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 502 Service Unavailable', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 502,
|
||||
data: { message: 'Service unavailable' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('服务暂时不可用,请稍后重试')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Service Unavailable:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 503 Service Unavailable', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 503,
|
||||
data: { message: 'Service unavailable' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('服务暂时不可用,请稍后重试')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Service Unavailable:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle 504 Gateway Timeout', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 504,
|
||||
data: { message: 'Gateway timeout' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('服务暂时不可用,请稍后重试')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Service Unavailable:', mockError.response.data)
|
||||
})
|
||||
|
||||
it('should handle unknown status code', () => {
|
||||
const mockError = {
|
||||
response: {
|
||||
status: 418,
|
||||
data: { message: 'I am a teapot' },
|
||||
},
|
||||
}
|
||||
const consoleSpy = vi.spyOn(console, 'error')
|
||||
|
||||
ApiErrorHandler.handle(mockError)
|
||||
|
||||
expect(ElMessage.error).toHaveBeenCalledWith('I am a teapot')
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Unknown Error:', mockError.response.data)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -74,7 +74,11 @@
|
||||
sortable="custom"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="120"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -103,7 +107,10 @@
|
||||
title="异常详情"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions
|
||||
:column="1"
|
||||
border
|
||||
>
|
||||
<el-descriptions-item label="ID">
|
||||
{{ currentDetail.id }}
|
||||
</el-descriptions-item>
|
||||
@@ -120,7 +127,9 @@
|
||||
<pre style="max-height: 200px; overflow: auto;">{{ currentDetail.params }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常信息">
|
||||
<div style="color: #f56c6c; word-break: break-all;">{{ currentDetail.errorMsg }}</div>
|
||||
<div style="color: #f56c6c; word-break: break-all;">
|
||||
{{ currentDetail.errorMsg }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常堆栈">
|
||||
<pre style="max-height: 300px; overflow: auto; font-size: 12px;">{{ currentDetail.exceptionStack }}</pre>
|
||||
@@ -133,7 +142,9 @@
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button @click="detailVisible = false">
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
@@ -66,14 +66,19 @@
|
||||
:show-overflow-tooltip="true"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.params" class="params-content">
|
||||
<div
|
||||
v-if="row.params"
|
||||
class="params-content"
|
||||
>
|
||||
<el-popover
|
||||
placement="top"
|
||||
:width="500"
|
||||
trigger="hover"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="params-preview">{{ formatParams(row.params) }}</div>
|
||||
<div class="params-preview">
|
||||
{{ formatParams(row.params) }}
|
||||
</div>
|
||||
</template>
|
||||
<pre class="params-detail">{{ formatParams(row.params) }}</pre>
|
||||
</el-popover>
|
||||
|
||||
@@ -2,49 +2,69 @@
|
||||
<div class="dashboard">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-card v-loading="loading" class="stat-card user-card">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card user-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="用户总数"
|
||||
:value="stats.userCount"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon user-icon"><User /></el-icon>
|
||||
<el-icon class="stat-icon user-icon">
|
||||
<User />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card v-loading="loading" class="stat-card role-card">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card role-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="角色总数"
|
||||
:value="stats.roleCount"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon role-icon"><UserFilled /></el-icon>
|
||||
<el-icon class="stat-icon role-icon">
|
||||
<UserFilled />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card v-loading="loading" class="stat-card login-card">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card login-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="今日登录"
|
||||
:value="stats.todayLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon login-icon"><ArrowRight /></el-icon>
|
||||
<el-icon class="stat-icon login-icon">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card v-loading="loading" class="stat-card log-card">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card log-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="操作日志"
|
||||
:value="stats.operationLog"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon log-icon"><Document /></el-icon>
|
||||
<el-icon class="stat-icon log-icon">
|
||||
<Document />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
@@ -63,7 +83,9 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">最近登录</span>
|
||||
<el-icon class="header-icon"><Clock /></el-icon>
|
||||
<el-icon class="header-icon">
|
||||
<Clock />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<el-timeline>
|
||||
@@ -85,8 +107,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item v-if="recentLogins.length === 0" placement="top">
|
||||
<div class="empty-tip">暂无登录记录</div>
|
||||
<el-timeline-item
|
||||
v-if="recentLogins.length === 0"
|
||||
placement="top"
|
||||
>
|
||||
<div class="empty-tip">
|
||||
暂无登录记录
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
@@ -100,7 +127,9 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">系统信息</span>
|
||||
<el-icon class="header-icon"><Setting /></el-icon>
|
||||
<el-icon class="header-icon">
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions
|
||||
|
||||
@@ -44,18 +44,19 @@
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="name"
|
||||
prop="roleName"
|
||||
label="角色名称"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="code"
|
||||
prop="roleKey"
|
||||
label="角色标识"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="description"
|
||||
label="描述"
|
||||
prop="roleSort"
|
||||
label="显示顺序"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
label="状态"
|
||||
@@ -63,11 +64,11 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === 'ACTIVE' ? 'success' : 'danger'"
|
||||
:type="row.status === RoleStatus.ACTIVE ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === 'ACTIVE' ? '正常' : '禁用' }}
|
||||
{{ row.status === RoleStatus.ACTIVE ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -130,21 +131,24 @@
|
||||
label="角色名称"
|
||||
required
|
||||
>
|
||||
<el-input v-model="formState.name" />
|
||||
<el-input v-model="formState.roleName" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="角色标识"
|
||||
required
|
||||
>
|
||||
<el-input
|
||||
v-model="formState.code"
|
||||
v-model="formState.roleKey"
|
||||
:disabled="!!formState.id"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input
|
||||
v-model="formState.description"
|
||||
type="textarea"
|
||||
<el-form-item
|
||||
label="显示顺序"
|
||||
required
|
||||
>
|
||||
<el-input-number
|
||||
v-model="formState.roleSort"
|
||||
:min="1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
@@ -208,6 +212,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { roleApi, type Role, type CreateRoleRequest, type UpdateRoleRequest, type Permission } from '@/api/role.api'
|
||||
import { handleApiError } from '@/utils/errorHandler'
|
||||
import { RoleStatus } from '@/constants/status'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<Role[]>([])
|
||||
@@ -225,12 +230,12 @@ const sortInfo = reactive({
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formState = reactive<CreateRoleRequest & { id?: number }>({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
const formState = reactive<CreateRoleRequest & { id?: number; status?: RoleStatus }>({
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
roleSort: 1,
|
||||
permissions: [],
|
||||
status: 'ACTIVE'
|
||||
status: RoleStatus.ACTIVE
|
||||
})
|
||||
|
||||
const permissionDialogVisible = ref(false)
|
||||
@@ -282,11 +287,11 @@ const handleAdd = () => {
|
||||
modalTitle.value = '新增角色'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
roleSort: 1,
|
||||
permissions: [],
|
||||
status: 'ACTIVE'
|
||||
status: RoleStatus.ACTIVE
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
@@ -295,9 +300,9 @@ const handleEdit = (row: Role) => {
|
||||
modalTitle.value = '编辑角色'
|
||||
Object.assign(formState, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
description: row.description,
|
||||
roleName: row.roleName,
|
||||
roleKey: row.roleKey,
|
||||
roleSort: row.roleSort,
|
||||
status: row.status,
|
||||
permissions: []
|
||||
})
|
||||
@@ -325,17 +330,18 @@ const handleModalOk = async () => {
|
||||
try {
|
||||
if (formState.id) {
|
||||
const updateData: UpdateRoleRequest = {
|
||||
name: formState.name,
|
||||
description: formState.description,
|
||||
status: formState.status as 'ACTIVE' | 'INACTIVE'
|
||||
roleName: formState.roleName,
|
||||
roleKey: formState.roleKey,
|
||||
roleSort: formState.roleSort,
|
||||
status: formState.status
|
||||
}
|
||||
await roleApi.update(formState.id, updateData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
const createData: CreateRoleRequest = {
|
||||
name: formState.name,
|
||||
code: formState.code,
|
||||
description: formState.description,
|
||||
roleName: formState.roleName,
|
||||
roleKey: formState.roleKey,
|
||||
roleSort: formState.roleSort,
|
||||
permissions: formState.permissions
|
||||
}
|
||||
await roleApi.create(createData)
|
||||
|
||||
@@ -69,11 +69,11 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === 'ACTIVE' ? 'success' : 'danger'"
|
||||
:type="row.status === 1 ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === 'ACTIVE' ? '正常' : '禁用' }}
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -163,11 +163,11 @@
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
value="ACTIVE"
|
||||
:value="UserStatus.ACTIVE"
|
||||
label="正常"
|
||||
/>
|
||||
<el-option
|
||||
value="INACTIVE"
|
||||
:value="UserStatus.INACTIVE"
|
||||
label="禁用"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -218,6 +218,7 @@ import { Search } from '@element-plus/icons-vue'
|
||||
import { userApi, type User, type CreateUserRequest, type UpdateUserRequest } from '@/api/user.api'
|
||||
import { roleApi, type Role } from '@/api/role.api'
|
||||
import { handleApiError } from '@/utils/errorHandler'
|
||||
import { UserStatus, StatusHelper } from '@/constants/status'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<User[]>([])
|
||||
@@ -235,14 +236,14 @@ const sortInfo = reactive({
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formState = reactive<CreateUserRequest & { id?: number }>({
|
||||
const formState = reactive<CreateUserRequest & { id?: number; status?: UserStatus }>({
|
||||
username: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roles: [],
|
||||
status: 'ACTIVE'
|
||||
status: UserStatus.ACTIVE
|
||||
})
|
||||
|
||||
const roleDialogVisible = ref(false)
|
||||
@@ -342,7 +343,7 @@ const handleModalOk = async () => {
|
||||
nickname: formState.nickname,
|
||||
email: formState.email,
|
||||
phone: formState.phone,
|
||||
status: formState.status as 'ACTIVE' | 'INACTIVE'
|
||||
status: formState.status
|
||||
}
|
||||
await userApi.update(formState.id, updateData)
|
||||
ElMessage.success('更新成功')
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
pool: 'threads',
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: false,
|
||||
minThreads: 2,
|
||||
maxThreads: 4,
|
||||
useAtomics: true,
|
||||
},
|
||||
},
|
||||
include: ['src/test/**/*.{test,spec}.{js,ts}'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'**/*.d.ts',
|
||||
'**/*.config.*',
|
||||
'**/mockData',
|
||||
'e2e/',
|
||||
],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html', 'lcov'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'src/test/',
|
||||
'**/*.d.ts',
|
||||
'**/*.config.*',
|
||||
'**/mockData',
|
||||
'e2e/',
|
||||
'src/main.ts',
|
||||
'src/router/index.ts',
|
||||
],
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80,
|
||||
all: true,
|
||||
clean: true,
|
||||
reportsDirectory: './coverage',
|
||||
subdir: '.',
|
||||
},
|
||||
testTimeout: 10000,
|
||||
hookTimeout: 10000,
|
||||
teardownTimeout: 10000,
|
||||
isolate: false,
|
||||
bail: 5,
|
||||
retry: 2,
|
||||
reporters: ['verbose', 'json', 'html'],
|
||||
outputFile: {
|
||||
json: './test-results/vitest-results.json',
|
||||
html: './test-results/vitest-report.html',
|
||||
},
|
||||
maxConcurrency: 4,
|
||||
cache: {
|
||||
dir: './node_modules/.vitest',
|
||||
enabled: true,
|
||||
},
|
||||
benchmark: {
|
||||
include: ['src/test/**/*.{bench,benchmark}.{js,ts}'],
|
||||
exclude: ['node_modules/', 'dist/'],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html', 'lcov'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'src/test/',
|
||||
'**/*.d.ts',
|
||||
'**/*.config.*',
|
||||
'**/mockData',
|
||||
'e2e/',
|
||||
],
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user