develop #2

Merged
jenkins merged 49 commits from develop into main 2026-04-08 19:55:46 +08:00
144 changed files with 5535 additions and 11514 deletions
Vendored
+310
View File
@@ -0,0 +1,310 @@
pipeline {
agent any
environment {
// 项目配置
PROJECT_NAME = 'novalon-manage-system'
FRONTEND_DIR = 'novalon-manage-web'
BACKEND_DIR = 'novalon-manage-api'
// Node.js 配置
NODE_VERSION = '20'
PNPM_VERSION = '8.15.0'
// Java 配置
JAVA_VERSION = '17'
MAVEN_VERSION = '3.9.0'
// Docker 配置
DOCKER_REGISTRY = credentials('docker-registry')
DOCKER_IMAGE_FRONTEND = "${PROJECT_NAME}-frontend"
DOCKER_IMAGE_BACKEND = "${PROJECT_NAME}-backend"
// 数据库配置(用于E2E测试)
DB_HOST = 'localhost'
DB_PORT = '5432'
DB_NAME = 'novalon_test'
DB_USER = credentials('db-user')
DB_PASSWORD = credentials('db-password')
// 测试配置
TEST_TIMEOUT = '30'
RETRY_COUNT = '2'
}
tools {
nodejs "NodeJS-${NODE_VERSION}"
maven "Maven-${MAVEN_VERSION}"
jdk "JDK-${JAVA_VERSION}"
}
stages {
stage('环境准备') {
steps {
echo '🔧 准备构建环境...'
sh '''
# 安装 pnpm
npm install -g pnpm@${PNPM_VERSION}
# 验证工具版本
node --version
pnpm --version
java -version
mvn --version
'''
}
}
stage('代码检查') {
parallel {
stage('前端代码检查') {
steps {
dir(FRONTEND_DIR) {
echo '🔍 执行前端代码检查...'
sh '''
pnpm install
pnpm run lint
pnpm run type-check
'''
}
}
}
stage('后端代码检查') {
steps {
dir(BACKEND_DIR) {
echo '🔍 执行后端代码检查...'
sh 'mvn clean compile -DskipTests'
}
}
}
}
}
stage('单元测试') {
parallel {
stage('前端单元测试') {
steps {
dir(FRONTEND_DIR) {
echo '🧪 执行前端单元测试...'
sh 'pnpm run test:unit'
}
}
post {
always {
dir(FRONTEND_DIR) {
// 发布测试报告
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: '前端单元测试覆盖率报告'
])
}
}
}
}
stage('后端单元测试') {
steps {
dir(BACKEND_DIR) {
echo '🧪 执行后端单元测试...'
sh 'mvn test'
}
}
post {
always {
dir(BACKEND_DIR) {
// 发布测试报告
junit '**/target/surefire-reports/*.xml'
// 发布代码覆盖率报告
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'target/site/jacoco',
reportFiles: 'index.html',
reportName: '后端单元测试覆盖率报告'
])
}
}
}
}
}
}
stage('构建') {
parallel {
stage('前端构建') {
steps {
dir(FRONTEND_DIR) {
echo '📦 构建前端项目...'
sh '''
pnpm run build:prod
# 创建构建产物归档
tar -czf frontend-dist.tar.gz dist/
'''
}
}
post {
success {
archiveArtifacts artifacts: "${FRONTEND_DIR}/frontend-dist.tar.gz", fingerprint: true
}
}
}
stage('后端构建') {
steps {
dir(BACKEND_DIR) {
echo '📦 构建后端项目...'
sh '''
mvn clean package -DskipTests
# 创建构建产物归档
tar -czf backend-jars.tar.gz */target/*.jar
'''
}
}
post {
success {
archiveArtifacts artifacts: "${BACKEND_DIR}/backend-jars.tar.gz", fingerprint: true
}
}
}
}
}
stage('E2E测试') {
steps {
echo '🎭 执行E2E测试...'
dir(FRONTEND_DIR) {
sh '''
# 安装Playwright浏览器
pnpm exec playwright install --with-deps chromium
# 执行E2E测试
pnpm run test:e2e:journeys
'''
}
}
post {
always {
dir(FRONTEND_DIR) {
// 发布E2E测试报告
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'test-results',
reportFiles: 'custom-report.html',
reportName: 'E2E测试报告'
])
// 归档测试失败截图和视频
archiveArtifacts artifacts: 'test-results/**/*.png, test-results/**/*.webm', allowEmptyArchive: true
}
}
}
}
stage('构建Docker镜像') {
when {
branch 'develop'
}
steps {
echo '🐳 构建Docker镜像...'
// 构建前端镜像
dir(FRONTEND_DIR) {
sh """
docker build -t ${DOCKER_REGISTRY}/${DOCKER_IMAGE_FRONTEND}:${BUILD_NUMBER} .
docker tag ${DOCKER_REGISTRY}/${DOCKER_IMAGE_FRONTEND}:${BUILD_NUMBER} ${DOCKER_REGISTRY}/${DOCKER_IMAGE_FRONTEND}:latest
"""
}
// 构建后端镜像
dir(BACKEND_DIR) {
sh """
docker build -t ${DOCKER_REGISTRY}/${DOCKER_IMAGE_BACKEND}:${BUILD_NUMBER} .
docker tag ${DOCKER_REGISTRY}/${DOCKER_IMAGE_BACKEND}:${BUILD_NUMBER} ${DOCKER_REGISTRY}/${DOCKER_IMAGE_BACKEND}:latest
"""
}
}
}
stage('推送Docker镜像') {
when {
branch 'develop'
}
steps {
echo '📤 推送Docker镜像到仓库...'
sh """
docker push ${DOCKER_REGISTRY}/${DOCKER_IMAGE_FRONTEND}:${BUILD_NUMBER}
docker push ${DOCKER_REGISTRY}/${DOCKER_IMAGE_FRONTEND}:latest
docker push ${DOCKER_REGISTRY}/${DOCKER_IMAGE_BACKEND}:${BUILD_NUMBER}
docker push ${DOCKER_REGISTRY}/${DOCKER_IMAGE_BACKEND}:latest
"""
}
}
stage('部署到测试环境') {
when {
branch 'develop'
}
steps {
echo '🚀 部署到测试环境...'
sh """
# 这里可以添加部署脚本
# 例如:使用docker-compose或kubernetes部署
echo "部署前端镜像: ${DOCKER_REGISTRY}/${DOCKER_IMAGE_FRONTEND}:${BUILD_NUMBER}"
echo "部署后端镜像: ${DOCKER_REGISTRY}/${DOCKER_IMAGE_BACKEND}:${BUILD_NUMBER}"
"""
}
}
stage('部署到生产环境') {
when {
branch 'main'
}
steps {
echo '🚀 部署到生产环境...'
input message: '确认部署到生产环境?', ok: '确认部署'
sh """
# 这里可以添加生产环境部署脚本
# 例如:使用kubernetes进行滚动更新
echo "部署前端镜像: ${DOCKER_REGISTRY}/${DOCKER_IMAGE_FRONTEND}:${BUILD_NUMBER}"
echo "部署后端镜像: ${DOCKER_REGISTRY}/${DOCKER_IMAGE_BACKEND}:${BUILD_NUMBER}"
"""
}
}
}
post {
always {
echo '🧹 清理工作空间...'
cleanWs()
}
success {
echo '✅ 流水线执行成功!'
// 可以添加通知,例如发送邮件或Slack消息
}
failure {
echo '❌ 流水线执行失败!'
// 可以添加失败通知
}
unstable {
echo '⚠️ 流水线执行不稳定!'
// 可以添加不稳定状态通知
}
}
}
+122
View File
@@ -0,0 +1,122 @@
version: '3.8'
services:
# PostgreSQL 数据库(用于测试)
postgres:
image: postgres:15-alpine
container_name: novalon-test-db
environment:
POSTGRES_DB: novalon_test
POSTGRES_USER: novalon
POSTGRES_PASSWORD: novalon123
ports:
- "5432:5432"
volumes:
- postgres_test_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U novalon -d novalon_test"]
interval: 10s
timeout: 5s
retries: 5
networks:
- novalon-test-network
# Redis 缓存(可选)
redis:
image: redis:7-alpine
container_name: novalon-test-redis
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- novalon-test-network
# 后端服务
backend:
build:
context: ./novalon-manage-api
dockerfile: Dockerfile
container_name: novalon-test-backend
environment:
SPRING_PROFILES_ACTIVE: test
SPRING_R2DBC_URL: r2dbc:postgresql://postgres:5432/novalon_test
SPRING_R2DBC_USERNAME: novalon
SPRING_R2DBC_PASSWORD: novalon123
SPRING_FLYWAY_URL: jdbc:postgresql://postgres:5432/novalon_test
SPRING_FLYWAY_USER: novalon
SPRING_FLYWAY_PASSWORD: novalon123
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
ports:
- "8084:8084"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8084/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
networks:
- novalon-test-network
# 网关服务
gateway:
build:
context: ./novalon-manage-api/manage-gateway
dockerfile: Dockerfile
container_name: novalon-test-gateway
environment:
SPRING_PROFILES_ACTIVE: test
BACKEND_SERVICE_URL: http://backend:8084
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
ports:
- "8080:8080"
depends_on:
backend:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
networks:
- novalon-test-network
# 前端服务(开发模式)
frontend:
build:
context: ./novalon-manage-web
dockerfile: Dockerfile.dev
container_name: novalon-test-frontend
environment:
VITE_API_BASE_URL: http://gateway:8080
ports:
- "3002:3002"
volumes:
- ./novalon-manage-web:/app
- /app/node_modules
depends_on:
gateway:
condition: service_healthy
networks:
- novalon-test-network
volumes:
postgres_test_data:
driver: local
networks:
novalon-test-network:
driver: bridge
@@ -0,0 +1,363 @@
# E2E测试精简实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 将E2E测试从38个文件精简为5个核心测试文件,保留关键业务流程验证
**架构:** 采用分层测试策略,保留核心用户旅程测试和冒烟测试,删除非核心测试文件
**技术栈:** Playwright, TypeScript
---
## 文件结构
**创建文件:**
- `novalon-manage-web/e2e/smoke/login-logout.spec.ts` - 冒烟测试
**删除文件:**
- 34个非核心测试文件(详见设计文档第8节)
**修改文件:**
- `novalon-manage-web/package.json` - 更新测试脚本
---
## 任务 1:创建冒烟测试目录和文件
**文件:**
- 创建:`novalon-manage-web/e2e/smoke/login-logout.spec.ts`
- [ ] **步骤 1:创建smoke目录**
运行:`mkdir -p novalon-manage-web/e2e/smoke`
- [ ] **步骤 2:编写冒烟测试代码**
```typescript
import { test, expect } from '@playwright/test';
test.describe('冒烟测试 - 基础流程', () => {
test('管理员登录和登出', async ({ page }) => {
await test.step('导航到登录页面', async () => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
});
await test.step('输入登录信息', async () => {
await page.fill('input[type="text"]', 'admin');
await page.fill('input[type="password"]', 'Test@123');
});
await test.step('点击登录按钮', async () => {
await page.click('button:has-text("登录")');
await page.waitForURL(/.*dashboard/, { timeout: 10000 });
});
await test.step('验证登录成功', async () => {
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('点击用户菜单', async () => {
await page.click('[data-testid="user-menu"]');
await page.waitForTimeout(500);
});
await test.step('点击退出登录', async () => {
await page.click('text=退出登录');
await page.waitForURL(/.*login/, { timeout: 10000 });
});
await test.step('验证登出成功', async () => {
await expect(page).toHaveURL(/.*login/);
});
});
});
```
- [ ] **步骤 3Commit**
```bash
git add novalon-manage-web/e2e/smoke/login-logout.spec.ts
git commit -m "test: 添加冒烟测试 - 登录登出基础流程"
```
---
## 任务 2:删除根目录下的非核心测试文件
**文件:**
- 删除:`novalon-manage-web/e2e/auth.spec.ts`
- 删除:`novalon-manage-web/e2e/basic.spec.ts`
- 删除:`novalon-manage-web/e2e/complete-workflow.spec.ts`
- 删除:`novalon-manage-web/e2e/comprehensive-e2e.spec.ts`
- 删除:`novalon-manage-web/e2e/critical-e2e.spec.ts`
- 删除:`novalon-manage-web/e2e/dashboard-operation-log.spec.ts`
- 删除:`novalon-manage-web/e2e/dictionary-management.spec.ts`
- 删除:`novalon-manage-web/e2e/edge-cases.spec.ts`
- 删除:`novalon-manage-web/e2e/exception-log.spec.ts`
- 删除:`novalon-manage-web/e2e/file-management.spec.ts`
- 删除:`novalon-manage-web/e2e/form-test.spec.ts`
- 删除:`novalon-manage-web/e2e/login-log.spec.ts`
- 删除:`novalon-manage-web/e2e/menu-management.spec.ts`
- 删除:`novalon-manage-web/e2e/notification.spec.ts`
- 删除:`novalon-manage-web/e2e/operation-log.spec.ts`
- 删除:`novalon-manage-web/e2e/permission-validation.spec.ts`
- 删除:`novalon-manage-web/e2e/role-management.spec.ts`
- 删除:`novalon-manage-web/e2e/security-e2e.spec.ts`
- 删除:`novalon-manage-web/e2e/system-config.spec.ts`
- 删除:`novalon-manage-web/e2e/system-integration-test.spec.ts`
- 删除:`novalon-manage-web/e2e/test-config-api.spec.ts`
- 删除:`novalon-manage-web/e2e/test-stability.spec.ts`
- 删除:`novalon-manage-web/e2e/uat-file-workflow.spec.ts`
- 删除:`novalon-manage-web/e2e/uat-permission-workflow.spec.ts`
- 删除:`novalon-manage-web/e2e/uat-user-lifecycle.spec.ts`
- 删除:`novalon-manage-web/e2e/user-lifecycle.spec.ts`
- 删除:`novalon-manage-web/e2e/user-management.spec.ts`
- [ ] **步骤 1:删除根目录下的测试文件**
```bash
cd novalon-manage-web/e2e
rm -f auth.spec.ts basic.spec.ts complete-workflow.spec.ts comprehensive-e2e.spec.ts critical-e2e.spec.ts dashboard-operation-log.spec.ts dictionary-management.spec.ts edge-cases.spec.ts exception-log.spec.ts file-management.spec.ts form-test.spec.ts login-log.spec.ts menu-management.spec.ts notification.spec.ts operation-log.spec.ts permission-validation.spec.ts role-management.spec.ts security-e2e.spec.ts system-config.spec.ts system-integration-test.spec.ts test-config-api.spec.ts test-stability.spec.ts uat-file-workflow.spec.ts uat-permission-workflow.spec.ts uat-user-lifecycle.spec.ts user-lifecycle.spec.ts user-management.spec.ts
```
- [ ] **步骤 2Commit**
```bash
git add -A
git commit -m "test: 删除根目录下的非核心E2E测试文件"
```
---
## 任务 3:删除role-based-tests目录
**文件:**
- 删除:`novalon-manage-web/e2e/role-based-tests/` 整个目录
- [ ] **步骤 1:删除role-based-tests目录**
```bash
rm -rf novalon-manage-web/e2e/role-based-tests
```
- [ ] **步骤 2Commit**
```bash
git add -A
git commit -m "test: 删除role-based-tests目录"
```
---
## 任务 4:删除journeys目录下的重复测试文件
**文件:**
- 删除:`novalon-manage-web/e2e/journeys/system-config-workflow.spec.ts`
- 删除:`novalon-manage-web/e2e/journeys/permission-boundary.spec.ts`
- [ ] **步骤 1:删除重复的测试文件**
```bash
rm -f novalon-manage-web/e2e/journeys/system-config-workflow.spec.ts
rm -f novalon-manage-web/e2e/journeys/permission-boundary.spec.ts
```
- [ ] **步骤 2Commit**
```bash
git add -A
git commit -m "test: 删除journeys目录下的重复测试文件"
```
---
## 任务 5:更新package.json测试脚本
**文件:**
- 修改:`novalon-manage-web/package.json`
- [ ] **步骤 1:查看当前测试脚本**
运行:`cat novalon-manage-web/package.json | grep -A 10 '"scripts"'`
- [ ] **步骤 2:更新测试脚本**
`package.json``scripts` 部分添加或更新以下内容:
```json
{
"scripts": {
"test:e2e:smoke": "playwright test smoke/",
"test:e2e:journeys": "playwright test journeys/",
"test:e2e": "playwright test"
}
}
```
- [ ] **步骤 3:验证脚本更新**
运行:`cat novalon-manage-web/package.json | grep -A 5 '"test:e2e'`
- [ ] **步骤 4Commit**
```bash
git add novalon-manage-web/package.json
git commit -m "test: 更新E2E测试脚本,支持分层运行"
```
---
## 任务 6:验证测试运行
**文件:**
- 无文件变更
- [ ] **步骤 1:验证冒烟测试**
运行:`cd novalon-manage-web && npm run test:e2e:smoke`
预期:测试运行成功,1个测试通过
- [ ] **步骤 2:验证核心旅程测试**
运行:`cd novalon-manage-web && npm run test:e2e:journeys`
预期:测试运行成功,4个测试文件通过
- [ ] **步骤 3:验证所有测试**
运行:`cd novalon-manage-web && npm run test:e2e`
预期:测试运行成功,5个测试文件通过
---
## 任务 7:更新测试文档
**文件:**
- 创建:`novalon-manage-web/e2e/README.md`
- [ ] **步骤 1:编写测试文档**
```markdown
# E2E测试说明
## 测试结构
本项目的E2E测试采用分层测试策略:
### 冒烟测试(smoke/
快速验证基础功能是否正常工作。
- `login-logout.spec.ts` - 登录登出基础流程
### 核心旅程测试(journeys/
验证关键业务端到端流程。
- `admin-complete-workflow.spec.ts` - 管理员完整工作流
- `user-permission-boundary.spec.ts` - 用户权限边界验证
- `file-management-workflow.spec.ts` - 文件上传下载流程
- `audit-workflow.spec.ts` - 审计日志查看流程
## 运行测试
### 运行冒烟测试
```bash
npm run test:e2e:smoke
```
### 运行核心旅程测试
```bash
npm run test:e2e:journeys
```
### 运行所有测试
```bash
npm run test:e2e
```
## 测试数据
测试使用的用户账号:
- 管理员:username: `admin`, password: `Test@123`
- 普通用户:username: `user`, password: `Test@123`
## 测试策略
- **冒烟测试**:每次代码提交时运行,快速反馈
- **核心旅程测试**:PR合并前运行,验证关键业务流程
- **单元测试**:补充功能覆盖率,目标80%
## 维护指南
1. 新增核心业务功能时,在 `journeys/` 目录下添加测试
2. 新增基础功能时,在 `smoke/` 目录下添加测试
3. 保持测试文件数量精简,避免重复测试
4. 优先使用单元测试覆盖功能细节
```
- [ ] **步骤 2Commit**
```bash
git add novalon-manage-web/e2e/README.md
git commit -m "docs: 添加E2E测试说明文档"
```
---
## 任务 8:最终验证和清理
**文件:**
- 无文件变更
- [ ] **步骤 1:统计测试文件数量**
运行:`find novalon-manage-web/e2e -name "*.spec.ts" -type f | wc -l`
预期:输出 `5`
- [ ] **步骤 2:列出所有测试文件**
运行:`find novalon-manage-web/e2e -name "*.spec.ts" -type f`
预期输出:
```
novalon-manage-web/e2e/smoke/login-logout.spec.ts
novalon-manage-web/e2e/journeys/admin-complete-workflow.spec.ts
novalon-manage-web/e2e/journeys/user-permission-boundary.spec.ts
novalon-manage-web/e2e/journeys/file-management-workflow.spec.ts
novalon-manage-web/e2e/journeys/audit-workflow.spec.ts
```
- [ ] **步骤 3:运行完整测试套件**
运行:`cd novalon-manage-web && npm run test:e2e`
预期:所有测试通过
- [ ] **步骤 4:最终Commit**
```bash
git add -A
git commit -m "test: 完成E2E测试精简,从38个文件减少到5个"
```
---
## 预期成果
完成本计划后,将实现以下成果:
1. **测试文件数量**:从38个减少到5个(减少87%
2. **测试运行时间**:从~20分钟减少到~5分钟(减少75%)
3. **测试结构清晰**:冒烟测试 + 核心旅程测试
4. **维护成本降低**:测试文件数量少,易于维护
5. **测试稳定性提升**:减少flaky测试
@@ -0,0 +1,255 @@
# E2E测试精简设计文档
**版本:** 1.0
**日期:** 2026-04-07
**作者:** 张翔
**状态:** 待审查
---
## 1. 背景与目标
### 1.1 当前问题
当前E2E测试套件存在以下问题:
- **测试文件过多**:38个测试文件,维护成本高
- **运行时间长**:预计完整运行需要20分钟
- **测试稳定性差**:存在flaky测试,影响CI/CD效率
- **测试重复**:多个测试文件覆盖相同功能
### 1.2 优化目标
- 减少测试文件数量至5个(减少87%)
- 缩短测试运行时间至5分钟以内(减少75%)
- 提升测试稳定性和可维护性
- 保留关键业务流程验证
---
## 2. 测试架构设计
### 2.1 分层测试策略
采用分层测试策略,将E2E测试分为两层:
| 层级 | 测试类型 | 文件数 | 运行时间 | 覆盖范围 |
|------|---------|--------|---------|---------|
| L1 | 冒烟测试 | 1 | ~30秒 | 登录/登出基础流程 |
| L2 | 核心旅程 | 4 | ~4分钟 | 关键业务端到端流程 |
### 2.2 目录结构
```
e2e/
├── journeys/ # 核心用户旅程(保留)
│ ├── admin-complete-workflow.spec.ts # 管理员完整工作流
│ ├── user-permission-boundary.spec.ts # 用户权限边界验证
│ ├── file-management-workflow.spec.ts # 文件上传下载流程
│ └── audit-workflow.spec.ts # 审计日志查看流程
├── smoke/ # 冒烟测试(新增)
│ └── login-logout.spec.ts # 登录登出基础流程
├── fixtures/ # 测试数据(保留)
├── helpers/ # 测试辅助工具(保留)
├── pages/ # Page Object(保留)
└── utils/ # 工具函数(保留)
```
---
## 3. 核心测试用例设计
### 3.1 冒烟测试(smoke/login-logout.spec.ts
**测试目标:** 验证基础登录登出流程
**测试用例:**
- 管理员登录和登出
**预期运行时间:** ~30秒
### 3.2 核心旅程测试
#### 3.2.1 管理员完整工作流(admin-complete-workflow.spec.ts
**测试目标:** 验证管理员的核心操作流程
**测试用例:**
- 创建角色并分配权限
- 创建用户并分配角色
- 编辑用户信息
- 删除用户
- 删除角色
**预期运行时间:** ~2分钟
#### 3.2.2 用户权限边界验证(user-permission-boundary.spec.ts
**测试目标:** 验证权限控制是否正确
**测试用例:**
- 普通用户不能访问用户管理页面
- 普通用户不能访问角色管理页面
- 管理员可以访问所有页面
**预期运行时间:** ~1分钟
#### 3.2.3 文件管理流程(file-management-workflow.spec.ts
**测试目标:** 验证文件上传下载流程
**测试用例:**
- 上传文件
- 下载文件
- 删除文件
**预期运行时间:** ~1分钟
#### 3.2.4 审计日志流程(audit-workflow.spec.ts
**测试目标:** 验证审计日志查看功能
**测试用例:**
- 查看操作日志
- 查看登录日志
- 查看异常日志
**预期运行时间:** ~30秒
---
## 4. 实施计划
### 4.1 实施步骤
1. **创建新目录结构**
- 创建 `e2e/smoke/` 目录
2. **创建冒烟测试**
- 新建 `e2e/smoke/login-logout.spec.ts`
3. **删除非核心测试文件**
- 删除34个非核心测试文件
- 只保留 `journeys/` 目录下的4个核心测试文件
### 4.2 测试配置更新
**package.json 脚本更新:**
```json
{
"scripts": {
"test:e2e:smoke": "playwright test smoke/",
"test:e2e:journeys": "playwright test journeys/",
"test:e2e": "playwright test"
}
}
```
### 4.3 CI/CD集成
- **PR验证**:运行 `npm run test:e2e`~5分钟)
- **发布前验证**:运行所有测试
---
## 5. 预期收益
| 指标 | 优化前 | 优化后 | 改善幅度 |
|------|--------|--------|---------|
| 测试文件数量 | 38个 | 5个 | ↓ 87% |
| 预计运行时间 | ~20分钟 | ~5分钟 | ↓ 75% |
| 维护成本 | 高 | 低 | ↓ 80% |
| 测试稳定性 | 中 | 高 | ↑ 显著提升 |
---
## 6. 风险控制
### 6.1 功能覆盖风险
**风险:** 删除测试后功能覆盖下降
**缓解措施:**
- 通过单元测试和集成测试补充覆盖率
- 单元测试覆盖率目标:80%
### 6.2 回归测试风险
**风险:** 可能遗漏部分边界情况
**缓解措施:**
- 核心旅程测试覆盖关键路径
- 定期人工回归测试
### 6.3 团队适应风险
**风险:** 团队需要适应新的测试策略
**缓解措施:**
- 更新测试文档
- 培训团队成员
---
## 7. 后续优化建议
1. **补充单元测试**
- 为核心业务逻辑补充单元测试
- 覆盖率目标:80%
2. **补充集成测试**
- 为API接口补充集成测试
- 覆盖所有REST API端点
3. **持续优化**
- 定期评估测试效果
- 持续优化测试用例
---
## 8. 待删除测试文件清单
以下34个测试文件将被删除:
1. auth.spec.ts
2. basic.spec.ts
3. complete-workflow.spec.ts
4. comprehensive-e2e.spec.ts
5. critical-e2e.spec.ts
6. dashboard-operation-log.spec.ts
7. dictionary-management.spec.ts
8. edge-cases.spec.ts
9. exception-log.spec.ts
10. file-management.spec.ts
11. form-test.spec.ts
12. login-log.spec.ts
13. menu-management.spec.ts
14. notification.spec.ts
15. operation-log.spec.ts
16. permission-validation.spec.ts
17. role-management.spec.ts
18. security-e2e.spec.ts
19. system-config.spec.ts
20. system-integration-test.spec.ts
21. test-config-api.spec.ts
22. test-stability.spec.ts
23. uat-file-workflow.spec.ts
24. uat-permission-workflow.spec.ts
25. uat-user-lifecycle.spec.ts
26. user-lifecycle.spec.ts
27. user-management.spec.ts
28. role-based-tests/scenarios/authentication/login-flow.spec.ts
29. role-based-tests/scenarios/authentication/logout-flow.spec.ts
30. role-based-tests/scenarios/user-management/admin-creates-user.spec.ts
31. role-based-tests/scenarios/user-management/permission-boundary.spec.ts
32. journeys/system-config-workflow.spec.ts
33. journeys/permission-boundary.spec.ts(与user-permission-boundary.spec.ts重复)
---
## 9. 审查记录
| 日期 | 审查人 | 状态 | 备注 |
|------|--------|------|------|
| 2026-04-07 | 张翔 | 待审查 | 初始版本 |
@@ -0,0 +1,538 @@
# 权限系统增强设计文档
**日期**: 2026-04-08
**作者**: 张翔
**版本**: 1.0
**状态**: 待审查
## 1. 概述
### 1.1 背景
当前系统已完成基础的路由权限控制,但存在以下优化空间:
1. **菜单硬编码** - 菜单在前端硬编码,无法根据用户角色动态显示
2. **权限数据分散** - 角色和权限信息存储在 localStorage,缺乏统一管理
3. **缺少按钮级权限控制** - 无法控制按钮级别的权限
4. **缺少 API 权限检查** - 前端调用 API 前未检查权限,可能发送无效请求
### 1.2 目标
实现完整的权限系统增强,包括:
1. **动态菜单渲染** - 从后端获取菜单数据,根据用户权限动态渲染
2. **权限缓存优化** - 使用 Pinia 统一管理权限数据,localStorage 持久化
3. **权限指令** - 提供 `v-permission` 指令实现按钮级权限控制
4. **API 权限检查** - 前端调用 API 前检查权限,减少无效请求
### 1.3 范围
**包含:**
- Permission Store (Pinia)
- v-permission 指令
- 动态菜单渲染
- API 权限检查工具
- 相关单元测试
**不包含:**
- 后端权限系统修改(仅需新增 API)
- 数据库权限表结构调整
- 其他业务功能开发
## 2. 架构设计
### 2.1 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ 前端应用 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 路由守卫 │ │ 权限指令 │ │ 动态菜单 │ │
│ │ (已完成) │ │ v-permission │ │ 渲染 │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Permission │ │
│ │ Store (Pinia) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ localStorage │ │
│ │ (持久化) │ │
│ └─────────────────┘ │
│ │
└───────────────────────────┬─────────────────────────────────┘
HTTP API │
┌───────────────────────────▼─────────────────────────────────┐
│ 后端服务 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ /auth/login │ │ /menus/user │ │ /permissions │ │
│ │ (已存在) │ │ (新增) │ │ (已存在) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ RBAC 权限系统 (角色-权限-菜单) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 数据流
```
登录成功
解析 JWT token 获取角色
调用 fetchUserMenus() 获取菜单和权限
存入 Store + localStorage
页面刷新时从 localStorage 恢复
```
## 3. 详细设计
### 3.1 Permission Store
**文件位置**: `src/stores/permission.ts`
**状态定义**:
```typescript
interface PermissionState {
roles: string[] // 用户角色
permissions: string[] // 用户权限码
menus: MenuItem[] // 用户菜单
loaded: boolean // 是否已加载
}
interface MenuItem {
id: number
name: string
path: string
icon?: string
parentId?: number
sort: number
children?: MenuItem[]
}
```
**核心 Actions**:
```typescript
// 初始化权限数据(从 localStorage 恢复)
initFromStorage(): void
// 登录后设置权限数据
setPermissionData(data: {
roles: string[]
permissions: string[]
menus: MenuItem[]
}): void
// 从后端刷新权限数据
async fetchUserMenus(): Promise<void>
// 清除权限数据(退出登录)
clearPermissionData(): void
// 权限检查方法
hasRole(role: string | string[]): boolean
hasPermission(permission: string | string[]): boolean
```
**持久化策略**:
- 登录时:将角色、权限、菜单数据存入 localStorage
- 页面刷新:Pinia 从 localStorage 恢复数据,立即渲染菜单
- 权限变更:提供刷新机制,同步更新 localStorage 和 Pinia
- 退出登录:清除所有数据
### 3.2 v-permission 指令
**文件位置**: `src/directives/permission.ts`
**用法**:
```vue
<!-- 角色检查 -->
<button v-permission:role="'admin'">管理员按钮</button>
<!-- 权限码检查 -->
<button v-permission:permission="'user:delete'">删除用户</button>
<!-- 支持数组满足任一条件 -->
<button v-permission:role="['admin', 'manager']">导出数据</button>
<button v-permission:permission="['user:create', 'user:update']">编辑用户</button>
<!-- 简写形式默认权限检查 -->
<button v-permission="'user:delete'">删除</button>
```
**实现逻辑**:
```typescript
export const permissionDirective = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const permissionStore = usePermissionStore()
const { arg, value } = binding
const checkType = arg || 'permission' // 默认权限检查
let hasAccess = false
if (checkType === 'role') {
hasAccess = permissionStore.hasRole(value)
} else if (checkType === 'permission') {
hasAccess = permissionStore.hasPermission(value)
}
if (!hasAccess) {
el.style.display = 'none'
}
}
}
```
**注册方式**:
```typescript
// src/main.ts
import { permissionDirective } from '@/directives/permission'
app.directive('permission', permissionDirective)
```
### 3.3 动态菜单渲染
**后端 API**:
```
GET /api/menus/user
请求头:
Authorization: Bearer <token>
响应:
{
"code": 200,
"data": {
"menus": [
{
"id": 1,
"name": "仪表盘",
"path": "/dashboard",
"icon": "Odometer",
"parentId": null,
"sort": 1
},
{
"id": 2,
"name": "系统管理",
"path": "/system",
"icon": "Setting",
"parentId": null,
"sort": 2,
"children": [
{
"id": 3,
"name": "用户管理",
"path": "/users",
"icon": null,
"parentId": 2,
"sort": 1
}
]
}
],
"permissions": [
"user:read",
"user:create",
"user:update",
"user:delete"
]
}
}
```
**前端组件**:
```vue
<!-- src/layouts/DefaultLayout.vue -->
<template>
<el-menu
:default-active="activeMenu"
class="menu"
:collapse="collapsed"
router
>
<menu-item
v-for="menu in menuTree"
:key="menu.id"
:menu="menu"
/>
</el-menu>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { usePermissionStore } from '@/stores/permission'
import MenuItem from '@/components/MenuItem.vue'
const permissionStore = usePermissionStore()
const menuTree = computed(() => permissionStore.menus)
</script>
```
**递归菜单组件**:
```vue
<!-- src/components/MenuItem.vue -->
<template>
<!-- 有子菜单 -->
<el-sub-menu
v-if="menu.children && menu.children.length > 0"
:index="String(menu.id)"
>
<template #title>
<el-icon v-if="menu.icon">
<component :is="menu.icon" />
</el-icon>
<span>{{ menu.name }}</span>
</template>
<menu-item
v-for="child in menu.children"
:key="child.id"
:menu="child"
/>
</el-sub-menu>
<!-- 无子菜单 -->
<el-menu-item
v-else
:index="menu.path"
>
<el-icon v-if="menu.icon">
<component :is="menu.icon" />
</el-icon>
<span>{{ menu.name }}</span>
</el-menu-item>
</template>
```
### 3.4 API 权限检查
**文件位置**: `src/utils/permission-check.ts`
**权限映射配置**:
```typescript
const apiPermissionMap: Record<string, { permission: string; method: string }> = {
'/api/users:GET': { permission: 'user:read', method: 'GET' },
'/api/users:POST': { permission: 'user:create', method: 'POST' },
'/api/users/*:PUT': { permission: 'user:update', method: 'PUT' },
'/api/users/*:DELETE': { permission: 'user:delete', method: 'DELETE' },
'/api/roles:GET': { permission: 'role:read', method: 'GET' },
// ... 更多映射
}
```
**检查函数**:
```typescript
export function canAccessApi(path: string, method: string): boolean {
const permissionStore = usePermissionStore()
const required = findRequiredPermission(path, method, apiPermissionMap)
if (!required) {
return true // 未定义权限要求的 API 默认允许
}
return permissionStore.hasPermission(required.permission)
}
```
**集成到请求拦截器**:
```typescript
// src/utils/request.ts
import { canAccessApi } from './permission-check'
request.interceptors.request.use(
(config) => {
// 权限检查
const path = config.url || ''
const method = config.method?.toUpperCase() || 'GET'
if (!canAccessApi(path, method)) {
return Promise.reject(new Error('无权限访问此 API'))
}
// 原有的 token 和签名逻辑
// ...
return config
}
)
```
## 4. 测试策略
### 4.1 测试覆盖范围
1. **Permission Store 单元测试**
- 测试权限数据的存储和恢复
- 测试 hasRole 和 hasPermission 方法
- 测试 localStorage 持久化
- 测试数据清除功能
2. **v-permission 指令测试**
- 测试角色检查功能
- 测试权限码检查功能
- 测试数组参数处理
- 测试元素隐藏/显示逻辑
3. **动态菜单测试**
- 测试菜单数据获取
- 测试菜单树渲染
- 测试菜单缓存机制
- 测试菜单权限过滤
4. **API 权限检查测试**
- 测试权限映射匹配
- 测试通配符匹配
- 测试请求拦截逻辑
### 4.2 测试文件结构
```
src/
├── stores/
│ └── __tests__/
│ └── permission.test.ts
├── directives/
│ └── __tests__/
│ └── permission.test.ts
├── components/
│ └── __tests__/
│ └── MenuItem.test.ts
└── utils/
└── __tests__/
└── permission-check.test.ts
```
## 5. 实施计划
### 5.1 实施顺序
**第 1 步:Permission Store1-2 小时)**
- 创建 `src/stores/permission.ts`
- 实现 localStorage 持久化
- 编写单元测试
- 集成到登录流程
**第 2 步:v-permission 指令(1-2 小时)**
- 创建 `src/directives/permission.ts`
- 注册全局指令
- 编写单元测试
- 在现有页面应用示例
**第 3 步:后端 API 开发(2-3 小时)**
- 新增 `GET /api/menus/user` 接口
- 根据用户角色返回菜单树
- 返回用户权限列表
- 编写后端测试
**第 4 步:动态菜单渲染(2-3 小时)**
- 创建 `src/components/MenuItem.vue`
- 修改 `DefaultLayout.vue`
- 集成 Permission Store
- 编写组件测试
**第 5 步:API 权限检查(1-2 小时)**
- 创建 `src/utils/permission-check.ts`
- 集成到请求拦截器
- 编写单元测试
- 优化性能
### 5.2 后端 API 需求
**接口**: `GET /api/menus/user`
**功能**: 获取当前登录用户可访问的菜单和权限
**业务逻辑**:
1. 从 token 获取用户 ID
2. 查询用户角色
3. 根据角色查询菜单和权限
4. 构建菜单树结构
5. 返回菜单和权限列表
**预估时间**: 7-12 小时
## 6. 风险和约束
### 6.1 技术风险
1. **后端 API 开发时间** - 需要后端配合开发新 API
2. **菜单数据迁移** - 需要将硬编码菜单迁移到数据库
3. **权限数据同步** - 前后端权限数据需要保持一致
### 6.2 约束条件
1. **向后兼容** - 需要兼容现有的路由守卫逻辑
2. **性能要求** - 菜单加载不能影响页面首屏渲染速度
3. **测试覆盖** - 所有新增代码需要单元测试覆盖
## 7. 验收标准
### 7.1 功能验收
- [ ] Permission Store 正确管理权限数据
- [ ] v-permission 指令正确控制按钮显示
- [ ] 动态菜单根据用户权限正确渲染
- [ ] API 权限检查正确拦截无权限请求
### 7.2 质量验收
- [ ] 所有单元测试通过
- [ ] 代码覆盖率 ≥ 80%
- [ ] TypeScript 类型检查通过
- [ ] ESLint 检查通过
### 7.3 性能验收
- [ ] 菜单加载时间 < 500ms
- [ ] localStorage 读写不影响页面性能
- [ ] 权限检查不影响 API 请求速度
## 8. 后续优化
### 8.1 短期优化
1. **权限缓存过期** - 添加权限数据过期机制
2. **权限变更通知** - 实现权限变更后的实时通知
3. **权限日志** - 记录权限检查日志,便于调试
### 8.2 长期优化
1. **权限可视化配置** - 提供权限配置界面
2. **权限审计** - 记录用户权限变更历史
3. **权限模板** - 提供常用权限模板,简化配置
## 9. 参考资料
- [Vue 3 官方文档](https://vuejs.org/)
- [Pinia 官方文档](https://pinia.vuejs.org/)
- [Element Plus 文档](https://element-plus.org/)
- [RBAC 权限模型](https://en.wikipedia.org/wiki/Role-based_access_control)
+14
View File
@@ -0,0 +1,14 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class TestBCrypt {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String password = "admin123";
String hash = "$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy";
System.out.println("测试密码验证:");
System.out.println("密码: " + password);
System.out.println("哈希: " + hash);
System.out.println("验证结果: " + encoder.matches(password, hash));
}
}
@@ -1,42 +0,0 @@
package cn.novalon.manage.app.config;
import io.r2dbc.spi.ConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
/**
* R2DBC数据库初始化配置
*
* 用于测试环境的H2数据库初始化
*
* @author 张翔
* @date 2026-04-03
*/
@Configuration
@ConditionalOnProperty(name = "spring.profiles.active", havingValue = "test")
public class R2dbcInitConfig {
private static final Logger logger = LoggerFactory.getLogger(R2dbcInitConfig.class);
@Bean
public ConnectionFactoryInitializer connectionFactoryInitializer(ConnectionFactory connectionFactory) {
logger.info("Initializing R2DBC database with H2 schema and data");
ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
initializer.setConnectionFactory(connectionFactory);
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(new ClassPathResource("schema-h2.sql"));
populator.addScript(new ClassPathResource("data-h2.sql"));
initializer.setDatabasePopulator(populator);
return initializer;
}
}
@@ -5,6 +5,9 @@ spring:
password: novalon123
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
rate:
limit:
@@ -1,54 +0,0 @@
# H2数据库配置(用于测试环境)
spring:
r2dbc:
url: r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
pool:
initial-size: 5
max-size: 20
max-idle-time: 30m
max-life-time: 1h
acquire-timeout: 5s
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
path: /h2-console
settings:
web-allow-others: true
flyway:
enabled: false
sql:
init:
mode: always
continue-on-error: false
schema-locations: classpath:schema-h2.sql
data-locations: classpath:data-h2.sql
# 测试专用配置
test:
database:
type: h2
in-memory: true
cleanup:
enabled: true
strategy: truncate
# 日志配置
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.r2dbc: DEBUG
org.springframework.jdbc: DEBUG
org.flywaydb: INFO
com.h2database: WARN
@@ -5,26 +5,23 @@ spring:
application:
name: manage-app
r2dbc:
url: r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
pool:
initial-size: 5
max-size: 20
max-idle-time: 30m
max-life-time: 1h
acquire-timeout: 5s
datasource:
url: jdbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
flyway:
enabled: false
h2:
console:
enabled: true
path: /h2-console
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
sql:
init:
mode: never
security:
user:
name: disabled
@@ -2,7 +2,8 @@
-- 用于测试环境
-- 插入测试角色
INSERT INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by)
MERGE INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by)
KEY(id)
VALUES
(1, '超级管理员', 'admin', 1, 1, 'system', 'system'),
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system'),
@@ -11,7 +12,8 @@ VALUES
-- 插入测试用户
-- BCrypt哈希值对应明文密码: Test@123
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
MERGE INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
KEY(id)
VALUES
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'),
(2, 'testadmin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'testadmin@novalon.com', '13800138001', '测试管理员', 1, 'system', 'system'),
@@ -1,253 +0,0 @@
-- H2 Database Schema for Integration Testing
-- Create user table
CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
nickname VARCHAR(100),
role_id BIGINT,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create role table
CREATE TABLE IF NOT EXISTS sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0,
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create user role relation table
CREATE TABLE IF NOT EXISTS user_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
);
-- Create menu table
CREATE TABLE IF NOT EXISTS sys_menu (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
order_num INTEGER DEFAULT 0,
path VARCHAR(200),
component VARCHAR(200),
menu_type VARCHAR(1) DEFAULT 'C',
visible VARCHAR(1) DEFAULT '1',
status VARCHAR(1) DEFAULT '1',
perms VARCHAR(100),
icon VARCHAR(100),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create permission table
CREATE TABLE IF NOT EXISTS sys_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
permission_name VARCHAR(100) NOT NULL,
permission_code VARCHAR(100) NOT NULL UNIQUE,
resource VARCHAR(200),
action VARCHAR(20),
description VARCHAR(500),
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create role permission relation table
CREATE TABLE IF NOT EXISTS sys_role_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
updated_by VARCHAR(50),
CONSTRAINT fk_role_permission_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT fk_role_permission_permission FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
CONSTRAINT uk_role_permission UNIQUE (role_id, permission_id)
);
-- Create dict type table
CREATE TABLE IF NOT EXISTS sys_dict_type (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
dict_name VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(1) DEFAULT '0',
remark VARCHAR(500),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create dict data table
CREATE TABLE IF NOT EXISTS sys_dict_data (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
dict_sort INTEGER DEFAULT 0,
dict_label VARCHAR(100) NOT NULL,
dict_value VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL,
css_class VARCHAR(100),
list_class VARCHAR(100),
is_default VARCHAR(1) DEFAULT 'N',
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create dictionary table (general)
CREATE TABLE IF NOT EXISTS sys_dictionary (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
dict_value VARCHAR(500),
remark VARCHAR(500),
sort INTEGER DEFAULT 0,
create_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create system config table
CREATE TABLE IF NOT EXISTS sys_config (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
config_name VARCHAR(100) NOT NULL,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value VARCHAR(500) NOT NULL,
config_type VARCHAR(1) DEFAULT 'N',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create login log table
CREATE TABLE IF NOT EXISTS sys_login_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
ip VARCHAR(50),
location VARCHAR(255),
browser VARCHAR(50),
os VARCHAR(50),
status VARCHAR(1),
message VARCHAR(255),
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create exception log table
CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
title VARCHAR(100),
exception_name VARCHAR(100),
method_name VARCHAR(255),
method_params TEXT,
exception_msg TEXT,
exception_stack TEXT,
ip VARCHAR(50),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create operation log table
CREATE TABLE IF NOT EXISTS operation_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
operation VARCHAR(100),
method VARCHAR(200),
params TEXT,
result TEXT,
ip VARCHAR(50),
duration BIGINT,
status VARCHAR(1) DEFAULT '0',
error_msg TEXT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create system notice table
CREATE TABLE IF NOT EXISTS sys_notice (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
notice_title VARCHAR(50) NOT NULL,
notice_type VARCHAR(1) NOT NULL,
notice_content TEXT,
status VARCHAR(1) DEFAULT '0',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create user message table
CREATE TABLE IF NOT EXISTS sys_user_message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
notice_id BIGINT,
message_title VARCHAR(255),
message_content TEXT,
is_read VARCHAR(1) DEFAULT '0',
read_time TIMESTAMP,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create file management table
CREATE TABLE IF NOT EXISTS sys_file (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT,
file_type VARCHAR(100),
file_extension VARCHAR(10),
storage_type VARCHAR(50),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
CREATE INDEX IF NOT EXISTS idx_sys_dict_type ON sys_dict_data(dict_type);
CREATE INDEX IF NOT EXISTS idx_sys_login_log_username ON sys_login_log(username);
CREATE INDEX IF NOT EXISTS idx_operation_log_username ON operation_log(username);
@@ -1,30 +0,0 @@
package cn.novalon.manage.app.config;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
/**
* 测试数据库配置类
*
* 初始化H2内存数据库schema
*
* @author 张翔
* @date 2026-04-02
*/
@TestConfiguration
public class TestDatabaseConfig {
@Bean
public ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) {
ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
initializer.setConnectionFactory(connectionFactory);
initializer.setDatabasePopulator(new ResourceDatabasePopulator(
new ClassPathResource("schema-h2.sql"),
new ClassPathResource("data-h2.sql")));
return initializer;
}
}
@@ -1,6 +1,5 @@
package cn.novalon.manage.app.integration;
import cn.novalon.manage.app.config.TestDatabaseConfig;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.domain.SysRole;
@@ -14,7 +13,6 @@ import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
@@ -27,7 +25,7 @@ import static org.junit.jupiter.api.Assertions.*;
/**
* 用户服务集成测试
*
* 使用H2内存数据库进行集成测试
* 使用PostgreSQL数据库进行集成测试
*
* 注意:此测试需要完整的Spring上下文,暂时禁用。
* TODO: 优化集成测试配置
@@ -38,7 +36,6 @@ import static org.junit.jupiter.api.Assertions.*;
@Disabled("暂时禁用:集成测试配置需要优化")
@SpringBootTest
@ActiveProfiles("test")
@Import(TestDatabaseConfig.class)
class SysUserServiceIntegrationTest {
@Autowired
@@ -1,27 +1,22 @@
spring:
r2dbc:
url: r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
pool:
enabled: true
initial-size: 2
max-size: 10
h2:
console:
enabled: true
path: /h2-console
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
sql:
init:
mode: always
continue-on-error: false
schema-locations: classpath:schema-h2.sql
data-locations: classpath:data-h2.sql
flyway:
enabled: false
mode: never
security:
enabled: false
@@ -45,3 +45,32 @@ CREATE TABLE IF NOT EXISTS user_role (
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
-- 创建审计日志表
CREATE TABLE IF NOT EXISTS audit_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
entity_id BIGINT,
operation_type VARCHAR(20) NOT NULL,
operator VARCHAR(100),
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
before_data CLOB,
after_data CLOB,
changed_fields CLOB,
ip_address VARCHAR(50),
user_agent CLOB,
description CLOB,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建审计日志索引
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_type ON audit_log(entity_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_id ON audit_log(entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_type ON audit_log(operation_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_operator ON audit_log(operator);
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_time ON audit_log(operation_time);
CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);
+2 -2
View File
@@ -60,12 +60,12 @@
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
<scope>test</scope>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
@@ -0,0 +1,87 @@
package cn.novalon.manage.db.converter;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import cn.novalon.manage.db.entity.AuditLogEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 审计日志实体转换器
*
* @author 张翔
* @date 2026-04-08
*/
@Component
public class AuditLogConverter {
public AuditLog toDomain(AuditLogEntity entity) {
if (entity == null) {
return null;
}
AuditLog domain = new AuditLog();
domain.setId(entity.getId());
domain.setEntityType(entity.getEntityType());
domain.setEntityId(entity.getEntityId());
domain.setOperationType(entity.getOperationType());
domain.setOperator(entity.getOperator());
domain.setOperationTime(entity.getOperationTime());
domain.setBeforeData(entity.getBeforeData());
domain.setAfterData(entity.getAfterData());
domain.setChangedFields(entity.getChangedFields());
domain.setIpAddress(entity.getIpAddress());
domain.setUserAgent(entity.getUserAgent());
domain.setDescription(entity.getDescription());
domain.setCreateBy(entity.getCreateBy());
domain.setUpdateBy(entity.getUpdateBy());
domain.setCreatedAt(entity.getCreatedAt());
domain.setUpdatedAt(entity.getUpdatedAt());
domain.setDeletedAt(entity.getDeletedAt());
return domain;
}
public AuditLogEntity toEntity(AuditLog domain) {
if (domain == null) {
return null;
}
AuditLogEntity entity = new AuditLogEntity();
entity.setId(domain.getId());
entity.setEntityType(domain.getEntityType());
entity.setEntityId(domain.getEntityId());
entity.setOperationType(domain.getOperationType());
entity.setOperator(domain.getOperator());
entity.setOperationTime(domain.getOperationTime());
entity.setBeforeData(domain.getBeforeData());
entity.setAfterData(domain.getAfterData());
entity.setChangedFields(domain.getChangedFields());
entity.setIpAddress(domain.getIpAddress());
entity.setUserAgent(domain.getUserAgent());
entity.setDescription(domain.getDescription());
entity.setCreateBy(domain.getCreateBy());
entity.setUpdateBy(domain.getUpdateBy());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public List<AuditLog> toDomainList(List<AuditLogEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<AuditLogEntity> toEntityList(List<AuditLog> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,53 @@
package cn.novalon.manage.db.dao;
import cn.novalon.manage.db.entity.AuditLogEntity;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
/**
* 审计日志数据访问接口
*
* @author 张翔
* @date 2026-04-08
*/
@Repository
public interface AuditLogDao extends R2dbcRepository<AuditLogEntity, Long> {
Flux<AuditLogEntity> findByEntityTypeAndDeletedAtIsNull(String entityType);
Flux<AuditLogEntity> findByEntityIdAndDeletedAtIsNull(Long entityId);
Flux<AuditLogEntity> findByEntityTypeAndEntityIdAndDeletedAtIsNull(String entityType, Long entityId);
Flux<AuditLogEntity> findByOperatorAndDeletedAtIsNull(String operator);
Flux<AuditLogEntity> findByOperationTypeAndDeletedAtIsNull(String operationType);
Flux<AuditLogEntity> findByOperationTimeBetweenAndDeletedAtIsNull(LocalDateTime startTime, LocalDateTime endTime);
Flux<AuditLogEntity> findByEntityTypeAndOperationTimeBetweenAndDeletedAtIsNull(
String entityType,
LocalDateTime startTime,
LocalDateTime endTime
);
Flux<AuditLogEntity> findByOperatorAndOperationTimeBetweenAndDeletedAtIsNull(
String operator,
LocalDateTime startTime,
LocalDateTime endTime
);
Mono<Long> countByEntityTypeAndDeletedAtIsNull(String entityType);
Mono<Long> countByOperationTypeAndDeletedAtIsNull(String operationType);
Mono<Long> countByOperatorAndDeletedAtIsNull(String operator);
Mono<Long> countByOperationTimeBetweenAndDeletedAtIsNull(LocalDateTime startTime, LocalDateTime endTime);
Flux<AuditLogEntity> findByDeletedAtIsNull();
}
@@ -0,0 +1,135 @@
package cn.novalon.manage.db.entity;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
/**
* 审计日志数据库实体类
*
* @author 张翔
* @date 2026-04-08
*/
@Table("audit_log")
public class AuditLogEntity extends BaseEntity {
@Column("entity_type")
private String entityType;
@Column("entity_id")
private Long entityId;
@Column("operation_type")
private String operationType;
@Column("operator")
private String operator;
@Column("operation_time")
private java.time.LocalDateTime operationTime;
@Column("before_data")
private String beforeData;
@Column("after_data")
private String afterData;
@Column("changed_fields")
private String[] changedFields;
@Column("ip_address")
private String ipAddress;
@Column("user_agent")
private String userAgent;
@Column("description")
private String description;
public String getEntityType() {
return entityType;
}
public void setEntityType(String entityType) {
this.entityType = entityType;
}
public Long getEntityId() {
return entityId;
}
public void setEntityId(Long entityId) {
this.entityId = entityId;
}
public String getOperationType() {
return operationType;
}
public void setOperationType(String operationType) {
this.operationType = operationType;
}
public String getOperator() {
return operator;
}
public void setOperator(String operator) {
this.operator = operator;
}
public java.time.LocalDateTime getOperationTime() {
return operationTime;
}
public void setOperationTime(java.time.LocalDateTime operationTime) {
this.operationTime = operationTime;
}
public String getBeforeData() {
return beforeData;
}
public void setBeforeData(String beforeData) {
this.beforeData = beforeData;
}
public String getAfterData() {
return afterData;
}
public void setAfterData(String afterData) {
this.afterData = afterData;
}
public String[] getChangedFields() {
return changedFields;
}
public void setChangedFields(String[] changedFields) {
this.changedFields = changedFields;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
@@ -1,6 +1,11 @@
package cn.novalon.manage.db.entity;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.domain.Persistable;
import org.springframework.data.relational.core.mapping.Column;
import java.time.LocalDateTime;
@@ -11,26 +16,31 @@ import java.time.LocalDateTime;
* @author 张翔
* @date 2026-03-13
*/
public abstract class BaseEntity {
public abstract class BaseEntity implements Persistable<Long> {
@Id
private Long id;
@CreatedBy
@Column("create_by")
private String createBy;
@LastModifiedBy
@Column("update_by")
private String updateBy;
@CreatedDate
@Column("created_at")
private LocalDateTime createdAt;
@LastModifiedDate
@Column("updated_at")
private LocalDateTime updatedAt;
@Column("deleted_at")
private LocalDateTime deletedAt;
@Override
public Long getId() {
return id;
}
@@ -78,4 +88,13 @@ public abstract class BaseEntity {
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
/**
* 判断实体是否为新的
* 如果createdAt为null,则认为是新实体
*/
@Override
public boolean isNew() {
return createdAt == null;
}
}
@@ -0,0 +1,134 @@
package cn.novalon.manage.db.repository;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import cn.novalon.manage.sys.audit.repository.IAuditLogRepository;
import cn.novalon.manage.db.converter.AuditLogConverter;
import cn.novalon.manage.db.dao.AuditLogDao;
import cn.novalon.manage.db.entity.AuditLogEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
/**
* 审计日志仓储实现类
*
* @author 张翔
* @date 2026-04-08
*/
@Repository
public class AuditLogRepository implements IAuditLogRepository {
private static final Logger logger = LoggerFactory.getLogger(AuditLogRepository.class);
private final AuditLogDao auditLogDao;
private final AuditLogConverter auditLogConverter;
public AuditLogRepository(AuditLogDao auditLogDao, AuditLogConverter auditLogConverter) {
this.auditLogDao = auditLogDao;
this.auditLogConverter = auditLogConverter;
}
@Override
public Mono<AuditLog> findById(Long id) {
return auditLogDao.findById(id)
.map(auditLogConverter::toDomain);
}
@Override
public Mono<AuditLog> save(AuditLog auditLog) {
AuditLogEntity entity = auditLogConverter.toEntity(auditLog);
return auditLogDao.save(entity)
.map(auditLogConverter::toDomain);
}
@Override
public Mono<Void> deleteById(Long id) {
return auditLogDao.deleteById(id);
}
@Override
public Flux<AuditLog> findAll() {
return auditLogDao.findByDeletedAtIsNull()
.map(auditLogConverter::toDomain);
}
@Override
public Flux<AuditLog> findByEntityType(String entityType) {
return auditLogDao.findByEntityTypeAndDeletedAtIsNull(entityType)
.map(auditLogConverter::toDomain);
}
@Override
public Flux<AuditLog> findByEntityId(Long entityId) {
return auditLogDao.findByEntityIdAndDeletedAtIsNull(entityId)
.map(auditLogConverter::toDomain);
}
@Override
public Flux<AuditLog> findByEntityTypeAndEntityId(String entityType, Long entityId) {
return auditLogDao.findByEntityTypeAndEntityIdAndDeletedAtIsNull(entityType, entityId)
.map(auditLogConverter::toDomain);
}
@Override
public Flux<AuditLog> findByOperator(String operator) {
return auditLogDao.findByOperatorAndDeletedAtIsNull(operator)
.map(auditLogConverter::toDomain);
}
@Override
public Flux<AuditLog> findByOperationType(String operationType) {
return auditLogDao.findByOperationTypeAndDeletedAtIsNull(operationType)
.map(auditLogConverter::toDomain);
}
@Override
public Flux<AuditLog> findByOperationTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
return auditLogDao.findByOperationTimeBetweenAndDeletedAtIsNull(startTime, endTime)
.map(auditLogConverter::toDomain);
}
@Override
public Flux<AuditLog> findByEntityTypeAndOperationTimeBetween(
String entityType,
LocalDateTime startTime,
LocalDateTime endTime
) {
return auditLogDao.findByEntityTypeAndOperationTimeBetweenAndDeletedAtIsNull(entityType, startTime, endTime)
.map(auditLogConverter::toDomain);
}
@Override
public Flux<AuditLog> findByOperatorAndOperationTimeBetween(
String operator,
LocalDateTime startTime,
LocalDateTime endTime
) {
return auditLogDao.findByOperatorAndOperationTimeBetweenAndDeletedAtIsNull(operator, startTime, endTime)
.map(auditLogConverter::toDomain);
}
@Override
public Mono<Long> countByEntityType(String entityType) {
return auditLogDao.countByEntityTypeAndDeletedAtIsNull(entityType);
}
@Override
public Mono<Long> countByOperationType(String operationType) {
return auditLogDao.countByOperationTypeAndDeletedAtIsNull(operationType);
}
@Override
public Mono<Long> countByOperator(String operator) {
return auditLogDao.countByOperatorAndDeletedAtIsNull(operator);
}
@Override
public Mono<Long> countByOperationTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
return auditLogDao.countByOperationTimeBetweenAndDeletedAtIsNull(startTime, endTime);
}
}
@@ -0,0 +1,51 @@
-- Novalon管理系统普通用户角色和数据
-- 版本: V10
-- 描述: 创建普通用户角色并分配权限
-- 插入普通用户角色
INSERT INTO sys_role (role_name, role_key, role_sort, status, create_by, update_by)
VALUES ('普通用户', 'user', 2, 1, 'system', 'system')
ON CONFLICT (role_key) DO UPDATE SET
role_name = EXCLUDED.role_name,
role_sort = EXCLUDED.role_sort,
status = EXCLUDED.status;
-- 为普通用户分配基本权限(查看个人信息、修改密码等)
-- 注意:这里只分配基本权限,不包含管理功能权限
INSERT INTO sys_permission (permission_name, permission_key, permission_type, parent_id, path, component, icon, sort, status, create_by, update_by)
VALUES
('个人中心', 'profile', 'MENU', 0, '/profile', 'views/profile/index', 'user', 1, 1, 'system', 'system'),
('个人信息', 'profile:info', 'BUTTON', (SELECT id FROM sys_permission WHERE permission_key = 'profile'), '', '', '', 1, 1, 'system', 'system'),
('修改密码', 'profile:password', 'BUTTON', (SELECT id FROM sys_permission WHERE permission_key = 'profile'), '', '', '', 2, 1, 'system', 'system')
ON CONFLICT (permission_key) DO NOTHING;
-- 为普通用户角色分配权限
INSERT INTO sys_role_permission (role_id, permission_id, create_by, update_by)
SELECT
r.id as role_id,
p.id as permission_id,
'system' as create_by,
'system' as update_by
FROM sys_role r
CROSS JOIN sys_permission p
WHERE r.role_key = 'user'
AND p.permission_key IN ('profile', 'profile:info', 'profile:password')
ON CONFLICT DO NOTHING;
-- 将测试用户分配给普通用户角色
INSERT INTO user_role (user_id, role_id, create_by, update_by)
SELECT
u.id as user_id,
r.id as role_id,
'system' as create_by,
'system' as update_by
FROM sys_user u
CROSS JOIN sys_role r
WHERE u.username = 'user' AND r.role_key = 'user'
ON CONFLICT DO NOTHING;
-- 重置序列值
SELECT setval('sys_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role));
SELECT setval('sys_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_permission));
SELECT setval('sys_role_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role_permission));
SELECT setval('user_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM user_role));
@@ -0,0 +1,46 @@
-- Novalon管理系统测试数据脚本
-- 版本: V11
-- 描述: 更新测试用户密码为Test@123,插入E2E测试所需数据
-- 更新admin用户密码为Test@123
-- BCrypt哈希值对应明文密码: Test@123
UPDATE sys_user
SET password = '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C'
WHERE username = 'admin';
-- 更新user用户密码为Test@123
UPDATE sys_user
SET password = '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C'
WHERE username = 'user';
-- 插入测试角色(如果不存在)
INSERT INTO sys_role (role_name, role_key, role_sort, status, create_by, update_by)
VALUES
('测试管理员', 'test_admin', 2, 1, 'system', 'system'),
('普通用户', 'normal_user', 3, 1, 'system', 'system'),
('访客', 'guest', 4, 1, 'system', 'system')
ON CONFLICT (role_key) DO NOTHING;
-- 为admin用户分配超级管理员角色
INSERT INTO user_role (user_id, role_id, created_by)
SELECT 1, id, 'system' FROM sys_role WHERE role_key = 'admin'
ON CONFLICT DO NOTHING;
-- 为user用户分配普通用户角色
INSERT INTO user_role (user_id, role_id, created_by)
SELECT 2, id, 'system' FROM sys_role WHERE role_key = 'normal_user'
ON CONFLICT DO NOTHING;
-- 插入E2E测试专用用户
-- BCrypt哈希值对应明文密码: Test@123
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
VALUES
(10, 'e2e_test_user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'e2e@test.com', '13900139000', 'E2E测试用户', 1, 'system', 'system')
ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password,
status = EXCLUDED.status;
-- 为E2E测试用户分配超级管理员角色
INSERT INTO user_role (user_id, role_id, created_by)
SELECT 10, id, 'system' FROM sys_role WHERE role_key = 'admin'
ON CONFLICT DO NOTHING;
@@ -3,7 +3,7 @@
-- 描述: 创建所有核心表结构
-- 用户表
CREATE TABLE IF NOT EXISTS sys_user (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
@@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS sys_user (
);
-- 角色表
CREATE TABLE IF NOT EXISTS sys_role (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0,
@@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS sys_role (
);
-- 菜单表(统一使用sys_menu表名)
CREATE TABLE IF NOT EXISTS sys_menu (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
order_num INTEGER DEFAULT 0,
@@ -48,7 +48,7 @@ CREATE TABLE IF NOT EXISTS sys_menu (
);
-- 字典类型表
CREATE TABLE IF NOT EXISTS sys_dict_type (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
dict_name VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(1) DEFAULT '0',
@@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS sys_dict_type (
);
-- 字典数据表
CREATE TABLE IF NOT EXISTS sys_dict_data (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
dict_sort INTEGER DEFAULT 0,
dict_label VARCHAR(100) NOT NULL,
dict_value VARCHAR(100) NOT NULL,
@@ -78,7 +78,7 @@ CREATE TABLE IF NOT EXISTS sys_dict_data (
);
-- 字典表(通用字典)
CREATE TABLE IF NOT EXISTS sys_dictionary (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
@@ -92,7 +92,7 @@ CREATE TABLE IF NOT EXISTS sys_dictionary (
);
-- 系统配置表
CREATE TABLE IF NOT EXISTS sys_config (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
config_name VARCHAR(100) NOT NULL,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value VARCHAR(500) NOT NULL,
@@ -105,7 +105,7 @@ CREATE TABLE IF NOT EXISTS sys_config (
);
-- 登录日志表
CREATE TABLE IF NOT EXISTS sys_login_log (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
username VARCHAR(50),
ip VARCHAR(50),
location VARCHAR(255),
@@ -117,7 +117,7 @@ CREATE TABLE IF NOT EXISTS sys_login_log (
);
-- 异常日志表
CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
username VARCHAR(50),
title VARCHAR(100),
exception_name VARCHAR(100),
@@ -130,7 +130,7 @@ CREATE TABLE IF NOT EXISTS sys_exception_log (
);
-- 操作日志表
CREATE TABLE IF NOT EXISTS operation_log (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
username VARCHAR(50),
operation VARCHAR(100),
method VARCHAR(200),
@@ -148,7 +148,7 @@ CREATE TABLE IF NOT EXISTS operation_log (
);
-- 系统公告表
CREATE TABLE IF NOT EXISTS sys_notice (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
notice_title VARCHAR(50) NOT NULL,
notice_type VARCHAR(1) NOT NULL,
notice_content TEXT,
@@ -161,7 +161,7 @@ CREATE TABLE IF NOT EXISTS sys_notice (
);
-- 用户消息表
CREATE TABLE IF NOT EXISTS sys_user_message (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
notice_id BIGINT,
message_title VARCHAR(255),
@@ -176,7 +176,7 @@ CREATE TABLE IF NOT EXISTS sys_user_message (
);
-- 文件管理表
CREATE TABLE IF NOT EXISTS sys_file (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT,
@@ -191,7 +191,7 @@ CREATE TABLE IF NOT EXISTS sys_file (
);
-- OAuth2客户端表
CREATE TABLE IF NOT EXISTS oauth2_client (
id BIGSERIAL PRIMARY KEY,
id BIGINT PRIMARY KEY,
client_id VARCHAR(100) NOT NULL UNIQUE,
client_secret VARCHAR(255) NOT NULL,
client_name VARCHAR(100),
@@ -3,18 +3,26 @@
-- 描述: 插入必要的初始数据
-- 插入初始角色
INSERT INTO roles (role_name, role_key, role_sort, status, create_by, update_by)
INSERT INTO sys_role (role_name, role_key, role_sort, status, create_by, update_by)
VALUES ('超级管理员', 'admin', 1, 1, 'system', 'system')
ON CONFLICT (role_key) DO NOTHING;
-- 插入初始管理员用户
-- BCrypt哈希值对应明文密码: admin123
INSERT INTO users (id, username, password, email, phone, status, create_by, update_by)
INSERT INTO sys_user (id, username, password, email, phone, status, create_by, update_by)
VALUES (1, 'admin', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'admin@novalon.com', '13800138000', 1, 'system', 'system')
ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password,
status = EXCLUDED.status;
-- 插入测试用户(用于E2E测试)
-- BCrypt哈希值对应明文密码: admin123
INSERT INTO sys_user (id, username, password, email, phone, status, create_by, update_by)
VALUES (2, 'user', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'user@novalon.com', '13800138001', 1, 'system', 'system')
ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password,
status = EXCLUDED.status;
-- 插入初始字典类型
INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by)
VALUES
@@ -52,8 +60,8 @@ VALUES
ON CONFLICT (config_key) DO NOTHING;
-- 重置序列值
SELECT setval('users_id_seq', (SELECT COALESCE(MAX(id), 1) FROM users));
SELECT setval('roles_id_seq', (SELECT COALESCE(MAX(id), 1) FROM roles));
SELECT setval('sys_user_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_user));
SELECT setval('sys_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role));
SELECT setval('sys_dict_type_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_type));
SELECT setval('sys_dict_data_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_data));
SELECT setval('sys_config_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_config));
@@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS sys_role_permission (
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
UNIQUE (role_id, permission_id)
);
@@ -3,15 +3,15 @@
-- 描述: 为表创建必要的索引以提升查询性能
-- 用户表索引
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
CREATE INDEX IF NOT EXISTS idx_users_username ON sys_user(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON sys_user(email);
CREATE INDEX IF NOT EXISTS idx_users_status ON sys_user(status);
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON sys_user(deleted_at);
-- 角色表索引
CREATE INDEX IF NOT EXISTS idx_roles_role_key ON roles(role_key);
CREATE INDEX IF NOT EXISTS idx_roles_status ON roles(status);
CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON roles(deleted_at);
CREATE INDEX IF NOT EXISTS idx_roles_role_key ON sys_role(role_key);
CREATE INDEX IF NOT EXISTS idx_roles_status ON sys_role(status);
CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON sys_role(deleted_at);
-- 菜单表索引
CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
@@ -1,7 +1,6 @@
-- Novalon管理系统审计日志表
-- 版本: V7
-- 描述: 创建审计日志表,记录数据变更前后的完整对比
CREATE TABLE IF NOT EXISTS audit_log (
id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
@@ -11,20 +10,22 @@ CREATE TABLE IF NOT EXISTS audit_log (
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
before_data JSONB,
after_data JSONB,
changed_fields TEXT[],
changed_fields TEXT [],
ip_address VARCHAR(50),
user_agent TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE INDEX idx_audit_log_entity_type ON audit_log(entity_type);
CREATE INDEX idx_audit_log_entity_id ON audit_log(entity_id);
CREATE INDEX idx_audit_log_operation_type ON audit_log(operation_type);
CREATE INDEX idx_audit_log_operator ON audit_log(operator);
CREATE INDEX idx_audit_log_operation_time ON audit_log(operation_time);
CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id);
COMMENT ON TABLE audit_log IS '审计日志表';
COMMENT ON COLUMN audit_log.id IS '主键ID';
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)';
@@ -35,7 +36,5 @@ COMMENT ON COLUMN audit_log.operation_time IS '操作时间';
COMMENT ON COLUMN audit_log.before_data IS '变更前数据(JSON格式)';
COMMENT ON COLUMN audit_log.after_data IS '变更后数据(JSON格式)';
COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表';
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';
COMMENT ON COLUMN audit_log.user_agent IS '用户代理';
COMMENT ON COLUMN audit_log.description IS '操作描述';
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';COMMENT ON COLUMN audit_log.description IS '操作描述';
COMMENT ON COLUMN audit_log.created_at IS '记录创建时间';
@@ -0,0 +1,13 @@
-- Novalon管理系统权限授予脚本
-- 版本: V9
-- 描述: 为novalon用户授予所有表的访问权限
-- 授予所有表的SELECT, INSERT, UPDATE, DELETE权限
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO novalon;
-- 授予所有序列的使用权限
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO novalon;
-- 设置默认权限,使未来创建的表自动授予novalon用户权限
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO novalon;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO novalon;
@@ -1,7 +1,7 @@
package cn.novalon.manage.sys.audit;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import cn.novalon.manage.sys.audit.repository.IAuditLogRepository;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
@@ -34,11 +34,11 @@ public class AuditLogAspect {
private static final Logger logger = LoggerFactory.getLogger(AuditLogAspect.class);
private final IAuditLogRepository auditLogRepository;
private final IAuditLogService auditLogService;
private final ObjectMapper objectMapper;
public AuditLogAspect(IAuditLogRepository auditLogRepository, ObjectMapper objectMapper) {
this.auditLogRepository = auditLogRepository;
public AuditLogAspect(IAuditLogService auditLogService, ObjectMapper objectMapper) {
this.auditLogService = auditLogService;
this.objectMapper = objectMapper;
}
@@ -99,6 +99,9 @@ public class AuditLogAspect {
String finalOperationType = operationTypeHolder[0];
String finalBeforeData = beforeDataHolder[0];
logger.debug("保存操作审计日志: entityType={}, entityIdHolder={}, extractedEntityId={}, finalEntityId={}",
entityType, entityIdHolder[0], extractEntityId(savedEntity), finalEntityId);
return createAndSaveAuditLog(
entityType, finalEntityId, finalOperationType,
finalBeforeData, afterData, savedEntity
@@ -163,18 +166,22 @@ public class AuditLogAspect {
private Mono<Void> createAndSaveAuditLog(String entityType, Long entityId,
String operationType, String beforeData,
String afterData, Object entity) {
logger.debug("创建审计日志: entityType={}, entityId={}, operationType={}", entityType, entityId, operationType);
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal())
.defaultIfEmpty("system")
.flatMap(principal -> {
AuditLog auditLog = new AuditLog();
auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId);
auditLog.setEntityId(entityId != null ? entityId : 0L);
auditLog.setOperationType(operationType);
auditLog.setOperator(principal instanceof String ? (String) principal : "system");
auditLog.setBeforeData(beforeData);
auditLog.setAfterData(afterData);
logger.debug("审计日志对象: entityId={}, entityType={}, operationType={}",
auditLog.getEntityId(), auditLog.getEntityType(), auditLog.getOperationType());
if (beforeData != null && afterData != null) {
String[] changedFields = extractChangedFields(beforeData, afterData);
auditLog.setChangedFields(changedFields);
@@ -182,7 +189,7 @@ public class AuditLogAspect {
auditLog.setDescription(generateDescription(entityType, operationType, entityId));
return auditLogRepository.save(auditLog)
return auditLogService.save(auditLog)
.doOnSuccess(saved -> logger.debug("审计日志保存成功: {} - {}",
entityType, operationType))
.doOnError(error -> logger.error("审计日志保存失败: {}",
@@ -231,11 +238,14 @@ public class AuditLogAspect {
}
private Long extractEntityId(Object entity) {
logger.debug("提取实体ID: entity class={}", entity.getClass().getName());
if (entity instanceof Persistable) {
Persistable<?> persistable = (Persistable<?>) entity;
Object id = persistable.getId();
logger.debug("Persistable实体ID: id={}, isNew={}", id, persistable.isNew());
return id != null ? ((Number) id).longValue() : null;
}
logger.debug("实体不是Persistable类型");
return null;
}
@@ -1,9 +1,7 @@
package cn.novalon.manage.sys.audit.domain;
import cn.novalon.manage.sys.core.domain.BaseDomain;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@@ -13,73 +11,44 @@ import java.time.LocalDateTime;
* @author 张翔
* @date 2026-04-01
*/
@Table("audit_log")
@Schema(description = "审计日志实体")
public class AuditLog {
public class AuditLog extends BaseDomain {
@Id
@Schema(description = "主键ID")
private Long id;
@Column("entity_type")
@Schema(description = "实体类型(如User, Role等)", example = "User")
private String entityType;
@Column("entity_id")
@Schema(description = "实体ID", example = "1")
private Long entityId;
@Column("operation_type")
@Schema(description = "操作类型(CREATE, UPDATE, DELETE", example = "UPDATE")
private String operationType;
@Column("operator")
@Schema(description = "操作人", example = "admin")
private String operator;
@Column("operation_time")
@Schema(description = "操作时间")
private LocalDateTime operationTime;
@Column("before_data")
@Schema(description = "变更前数据(JSON格式)")
private String beforeData;
@Column("after_data")
@Schema(description = "变更后数据(JSON格式)")
private String afterData;
@Column("changed_fields")
@Schema(description = "变更字段列表")
private String[] changedFields;
@Column("ip_address")
@Schema(description = "IP地址", example = "192.168.1.100")
private String ipAddress;
@Column("user_agent")
@Schema(description = "用户代理")
private String userAgent;
@Column("description")
@Schema(description = "操作描述", example = "更新用户信息")
private String description;
@Column("created_at")
@Schema(description = "记录创建时间")
private LocalDateTime createdAt;
public AuditLog() {
this.operationTime = LocalDateTime.now();
this.createdAt = LocalDateTime.now();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getEntityType() {
@@ -169,12 +138,4 @@ public class AuditLog {
public void setDescription(String description) {
this.description = description;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}
@@ -1,8 +1,6 @@
package cn.novalon.manage.sys.audit.repository;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -14,8 +12,15 @@ import java.time.LocalDateTime;
* @author 张翔
* @date 2026-04-01
*/
@Repository
public interface IAuditLogRepository extends R2dbcRepository<AuditLog, Long> {
public interface IAuditLogRepository {
Mono<AuditLog> findById(Long id);
Mono<AuditLog> save(AuditLog auditLog);
Mono<Void> deleteById(Long id);
Flux<AuditLog> findAll();
Flux<AuditLog> findByEntityType(String entityType);
@@ -0,0 +1,30 @@
package cn.novalon.manage.sys.audit.service;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 审计日志服务接口
*
* @author 张翔
* @date 2026-04-08
*/
public interface IAuditLogService {
Mono<AuditLog> save(AuditLog auditLog);
Mono<AuditLog> findById(Long id);
Flux<AuditLog> findAll();
Flux<AuditLog> findByEntityType(String entityType);
Flux<AuditLog> findByEntityId(Long entityId);
Flux<AuditLog> findByEntityTypeAndEntityId(String entityType, Long entityId);
Flux<AuditLog> findByOperator(String operator);
Flux<AuditLog> findByOperationType(String operationType);
}
@@ -0,0 +1,68 @@
package cn.novalon.manage.sys.audit.service.impl;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import cn.novalon.manage.sys.audit.repository.IAuditLogRepository;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 审计日志服务实现类
*
* @author 张翔
* @date 2026-04-08
*/
@Service
public class AuditLogService implements IAuditLogService {
private static final Logger logger = LoggerFactory.getLogger(AuditLogService.class);
private final IAuditLogRepository auditLogRepository;
public AuditLogService(IAuditLogRepository auditLogRepository) {
this.auditLogRepository = auditLogRepository;
}
@Override
public Mono<AuditLog> save(AuditLog auditLog) {
return auditLogRepository.save(auditLog);
}
@Override
public Mono<AuditLog> findById(Long id) {
return auditLogRepository.findById(id);
}
@Override
public Flux<AuditLog> findAll() {
return auditLogRepository.findAll();
}
@Override
public Flux<AuditLog> findByEntityType(String entityType) {
return auditLogRepository.findByEntityType(entityType);
}
@Override
public Flux<AuditLog> findByEntityId(Long entityId) {
return auditLogRepository.findByEntityId(entityId);
}
@Override
public Flux<AuditLog> findByEntityTypeAndEntityId(String entityType, Long entityId) {
return auditLogRepository.findByEntityTypeAndEntityId(entityType, entityId);
}
@Override
public Flux<AuditLog> findByOperator(String operator) {
return auditLogRepository.findByOperator(operator);
}
@Override
public Flux<AuditLog> findByOperationType(String operationType) {
return auditLogRepository.findByOperationType(operationType);
}
}
@@ -1,5 +1,6 @@
package cn.novalon.manage.sys.core.domain;
import cn.novalon.manage.common.util.SnowflakeId;
import java.time.LocalDateTime;
/**
@@ -64,4 +65,14 @@ public abstract class BaseDomain {
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
}
@@ -78,16 +78,6 @@ public class SysPermission extends BaseDomain {
this.status = status;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
/**
* 删除权限
*/
@@ -58,16 +58,6 @@ public class SysRole extends BaseDomain {
this.status = status;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
/**
* 删除角色
*/
@@ -33,14 +33,4 @@ public class SysRolePermission extends BaseDomain {
public void setPermissionId(Long permissionId) {
this.permissionId = permissionId;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
}
@@ -101,16 +101,6 @@ public class SysUser extends BaseDomain {
this.status = status;
}
/**
* 生成主键ID
*
* @return 主键ID
*/
public Long generateId() {
this.id = SnowflakeId.nextId();
return this.id;
}
/**
* 删除用户
*/
@@ -82,6 +82,7 @@ public class SysRoleService implements ISysRoleService {
@Override
public Mono<SysRole> createRole(CreateRoleCommand command) {
SysRole role = new SysRole();
role.generateId();
role.setRoleName(command.roleName());
role.setRoleKey(command.roleKey());
role.setRoleSort(command.roleSort());
@@ -44,15 +44,15 @@ public class SysUserService implements ISysUserService {
private final IUserRoleRepository userRoleRepository;
private final PasswordEncoder passwordEncoder;
public SysUserService(ISysUserRepository userRepository,
ISysRoleRepository roleRepository,
IUserRoleRepository userRoleRepository,
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder) {
public SysUserService(ISysUserRepository userRepository,
ISysRoleRepository roleRepository,
IUserRoleRepository userRoleRepository,
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.userRoleRepository = userRoleRepository;
this.passwordEncoder = passwordEncoder;
logger.info("使用的密码编码器类型: {}", passwordEncoder.getClass().getName());
}
@@ -98,6 +98,7 @@ public class SysUserService implements ISysUserService {
logger.info("SysUserService.createUser - 用户名: {}, 密码前缀: {}",
user.getUsername(),
user.getPassword() != null ? user.getPassword().substring(0, 7) : "null");
user.generateId();
if (user.getPassword() != null && !user.getPassword().startsWith("$2a$")
&& !user.getPassword().startsWith("$2b$")) {
logger.info("密码不以$2a$或$2b$开头,重新编码");
@@ -106,7 +107,6 @@ public class SysUserService implements ISysUserService {
} else {
logger.info("密码已编码,跳过重新编码");
}
user.setCreatedAt(LocalDateTime.now());
if (user.getStatus() == null) {
user.setStatus(StatusConstants.ENABLED);
}
@@ -116,6 +116,7 @@ public class SysUserService implements ISysUserService {
@Override
public Mono<SysUser> createUser(CreateUserCommand command) {
SysUser user = new SysUser();
user.generateId();
user.setUsername(command.username().getValue());
user.setPassword(passwordEncoder.encode(command.password().getValue()));
user.setEmail(command.email().getValue());
@@ -123,7 +124,6 @@ public class SysUserService implements ISysUserService {
user.setPhone(command.phone());
user.setRoleId(command.roleId());
user.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
user.setCreatedAt(LocalDateTime.now());
return userRepository.save(user);
}
@@ -164,7 +164,7 @@ public class SysUserService implements ISysUserService {
@Transactional
public Mono<Void> deleteUser(Long id) {
logger.debug("开始删除用户,ID: {}", id);
return userRepository.findById(id)
.switchIfEmpty(Mono.error(new RuntimeException("User not found")))
.flatMap(user -> {
@@ -244,31 +244,30 @@ public class SysUserService implements ISysUserService {
@Transactional
public Mono<Void> assignRolesToUser(Long userId, List<Long> roleIds) {
logger.debug("开始为用户分配角色,用户ID: {}, 角色IDs: {}", userId, roleIds);
if (roleIds == null || roleIds.isEmpty()) {
logger.debug("角色列表为空,删除用户的所有角色关联");
return userRoleRepository.deleteByUserId(userId)
.doOnSuccess(v -> logger.debug("成功删除用户的所有角色关联"))
.doOnError(e -> logger.error("删除用户角色关联失败", e));
}
return userRoleRepository.deleteByUserId(userId)
.doOnSuccess(v -> logger.debug("成功删除用户的旧角色关联"))
.doOnError(e -> logger.error("删除用户旧角色关联失败", e))
.then(
Flux.fromIterable(roleIds)
.concatMap(roleId -> {
logger.debug("为用户分配角色ID: {}", roleId);
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRole.setCreatedAt(LocalDateTime.now());
return userRoleRepository.save(userRole)
.doOnSuccess(v -> logger.debug("成功保存用户角色关联"))
.doOnError(e -> logger.error("保存用户角色关联失败", e));
})
.then()
);
Flux.fromIterable(roleIds)
.concatMap(roleId -> {
logger.debug("为用户分配角色ID: {}", roleId);
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRole.setCreatedAt(LocalDateTime.now());
return userRoleRepository.save(userRole)
.doOnSuccess(v -> logger.debug("成功保存用户角色关联"))
.doOnError(e -> logger.error("保存用户角色关联失败", e));
})
.then());
}
@Override
@@ -0,0 +1,15 @@
package cn.novalon.manage.sys.dto.request;
import java.util.List;
public class AssignRolesRequest {
private List<Long> roleIds;
public List<Long> getRoleIds() {
return roleIds;
}
public void setRoleIds(List<Long> roleIds) {
this.roleIds = roleIds;
}
}
@@ -6,6 +6,8 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.util.List;
/**
* 用户注册请求DTO
*
@@ -42,6 +44,9 @@ public class UserRegisterRequest {
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Schema(description = "角色ID列表", example = "[1, 2]")
private List<Long> roles;
public String getUsername() {
return username;
}
@@ -81,4 +86,12 @@ public class UserRegisterRequest {
public void setPhone(String phone) {
this.phone = phone;
}
public List<Long> getRoles() {
return roles;
}
public void setRoles(List<Long> roles) {
this.roles = roles;
}
}
@@ -3,6 +3,7 @@ package cn.novalon.manage.sys.handler.user;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.service.ISysUserService;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.sys.dto.request.AssignRolesRequest;
import cn.novalon.manage.sys.dto.request.PasswordChangeRequest;
import cn.novalon.manage.sys.dto.request.UserRegisterRequest;
import cn.novalon.manage.sys.dto.request.UserUpdateRequest;
@@ -135,6 +136,14 @@ public class SysUserHandler {
null
))
.flatMap(userService::createUser)
.flatMap(user -> {
if (req.getRoles() != null && !req.getRoles().isEmpty()) {
logger.info("为用户 {} 分配角色: {}", user.getUsername(), req.getRoles());
return userService.assignRolesToUser(user.getId(), req.getRoles())
.then(Mono.just(user));
}
return Mono.just(user);
})
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
});
}
@@ -249,9 +258,8 @@ public class SysUserHandler {
@OperationLog(operation = "分配角色", module = "用户管理")
public Mono<ServerResponse> assignRoles(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(new org.springframework.core.ParameterizedTypeReference<List<Long>>() {
})
.flatMap(roleIds -> userService.assignRolesToUser(id, roleIds))
return request.bodyToMono(AssignRolesRequest.class)
.flatMap(req -> userService.assignRolesToUser(id, req.getRoleIds()))
.then(ServerResponse.ok().build())
.onErrorResume(error -> {
logger.error("分配角色失败", error);
+3
View File
@@ -34,3 +34,6 @@ TEST_WORKERS=4
# 测试报告配置(可选)
TEST_REPORT_FOLDER=playwright-report
TEST_RESULTS_FOLDER=test-results
# API签名密钥配置
VITE_SIGNATURE_SECRET=your-secret-key-here
+3
View File
@@ -2,7 +2,10 @@ node_modules
dist
.DS_Store
*.log
.env
.env.local
.env.*.local
coverage
.nyc_output
debug-*.png
e2e/debug/
+22 -6
View File
@@ -1,18 +1,34 @@
FROM node:18-alpine AS builder
# 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
# 安装 pnpm
RUN npm install -g pnpm@8.15.0
# 复制 package.json 和 lock 文件
COPY package.json pnpm-lock.yaml ./
# 安装依赖
RUN pnpm install
# 复制源代码
COPY . .
RUN npm run build
# 构建生产版本
RUN pnpm run build:prod
# 生产阶段
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制自定义 nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 暴露端口
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# 启动 nginx
CMD ["nginx", "-g", "daemon off;"]
+21
View File
@@ -0,0 +1,21 @@
FROM node:20-alpine
WORKDIR /app
# 安装 pnpm
RUN npm install -g pnpm@8.15.0
# 复制 package.json 和 lock 文件
COPY package.json pnpm-lock.yaml ./
# 安装依赖
RUN pnpm install
# 复制源代码
COPY . .
# 暴露端口
EXPOSE 3002
# 启动开发服务器
CMD ["pnpm", "run", "dev"]
+60
View File
@@ -0,0 +1,60 @@
# E2E测试说明
## 测试结构
本项目的E2E测试采用分层测试策略:
### 冒烟测试(smoke/
快速验证基础功能是否正常工作。
- `login-logout.spec.ts` - 登录登出基础流程
### 核心旅程测试(journeys/
验证关键业务端到端流程。
- `admin-complete-workflow.spec.ts` - 管理员完整工作流
- `user-permission-boundary.spec.ts` - 用户权限边界验证
- `file-management-workflow.spec.ts` - 文件上传下载流程
- `audit-workflow.spec.ts` - 审计日志查看流程
## 运行测试
### 运行冒烟测试
```bash
npm run test:e2e:smoke
```
### 运行核心旅程测试
```bash
npm run test:e2e:journeys
```
### 运行所有测试
```bash
npm run test:e2e
```
## 测试数据
测试使用的用户账号:
- 管理员:username: `admin`, password: `Test@123`
- 普通用户:username: `user`, password: `Test@123`
## 测试策略
- **冒烟测试**:每次代码提交时运行,快速反馈
- **核心旅程测试**:PR合并前运行,验证关键业务流程
- **单元测试**:补充功能覆盖率,目标80%
## 维护指南
1. 新增核心业务功能时,在 `journeys/` 目录下添加测试
2. 新增基础功能时,在 `smoke/` 目录下添加测试
3. 保持测试文件数量精简,避免重复测试
4. 优先使用单元测试覆盖功能细节
-202
View File
@@ -1,202 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { OperationLogPage } from './pages/OperationLogPage';
import { LoginLogPage } from './pages/LoginLogPage';
test.describe('审计功能 E2E 测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let operationLogPage: OperationLogPage;
let loginLogPage: LoginLogPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
operationLogPage = new OperationLogPage(page);
loginLogPage = new LoginLogPage(page);
});
test('AUDIT-001: 管理员查看操作日志', async ({ page }) => {
await test.step('管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('导航到操作日志页面', async () => {
await page.goto('/oplog');
await page.waitForLoadState('networkidle');
});
await test.step('验证操作日志页面加载', async () => {
await operationLogPage.goto();
await expect(operationLogPage.table).toBeVisible();
const rowCount = await operationLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
await test.step('验证日志表格包含必要列', async () => {
await expect(operationLogPage.table).toContainText('ID');
await expect(operationLogPage.table).toContainText('操作人');
await expect(operationLogPage.table).toContainText('操作模块');
await expect(operationLogPage.table).toContainText('请求方法');
});
});
test('AUDIT-002: 按关键词搜索操作日志', async ({ page }) => {
await test.step('管理员登录并导航到操作日志', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await operationLogPage.goto();
});
await test.step('搜索特定操作人', async () => {
await operationLogPage.searchByKeyword('admin');
await page.waitForTimeout(1000);
await operationLogPage.verifyTableContains('admin');
});
await test.step('清除搜索条件', async () => {
await operationLogPage.clearSearch();
const rowCount = await operationLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('AUDIT-003: 导出操作日志', async ({ page }) => {
await test.step('管理员登录并导航到操作日志', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await operationLogPage.goto();
});
await test.step('导出操作日志数据', async () => {
const downloadPromise = page.waitForEvent('download');
await operationLogPage.exportData();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.(xlsx|csv)$/);
});
});
test('AUDIT-004: 管理员查看登录日志', async ({ page }) => {
await test.step('管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('导航到登录日志页面', async () => {
await page.goto('/loginlog');
await page.waitForLoadState('networkidle');
});
await test.step('验证登录日志页面加载', async () => {
await loginLogPage.goto();
await expect(loginLogPage.table).toBeVisible();
const rowCount = await loginLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
await test.step('验证登录日志表格包含必要列', async () => {
await expect(loginLogPage.table).toContainText('ID');
await expect(loginLogPage.table).toContainText('用户名');
await expect(loginLogPage.table).toContainText('IP地址');
await expect(loginLogPage.table).toContainText('登录状态');
});
});
test('AUDIT-005: 按IP地址搜索登录日志', async ({ page }) => {
await test.step('管理员登录并导航到登录日志', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await loginLogPage.goto();
});
await test.step('搜索特定IP地址', async () => {
await loginLogPage.searchByKeyword('127.0.0.1');
await page.waitForTimeout(1000);
await loginLogPage.verifyTableContains('127.0.0.1');
});
await test.step('清除搜索条件', async () => {
await loginLogPage.clearSearch();
const rowCount = await loginLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('AUDIT-006: 导出登录日志', async ({ page }) => {
await test.step('管理员登录并导航到登录日志', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await loginLogPage.goto();
});
await test.step('导出登录日志数据', async () => {
const downloadPromise = page.waitForEvent('download');
await loginLogPage.exportData();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.(xlsx|csv)$/);
});
});
test('AUDIT-007: 验证审计权限控制', async ({ page }) => {
await test.step('普通用户登录', async () => {
await loginPage.goto();
await loginPage.login('user', 'user123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('尝试访问操作日志页面', async () => {
await page.goto('/oplog');
await page.waitForLoadState('networkidle');
const currentURL = page.url();
if (currentURL.includes('/oplog')) {
await expect(operationLogPage.table).toBeVisible();
} else {
await expect(page).toHaveURL(/.*dashboard/);
}
});
});
test('AUDIT-008: 验证操作日志时间排序', async ({ page }) => {
await test.step('管理员登录并导航到操作日志', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await operationLogPage.goto();
});
await test.step('验证日志按时间倒序排列', async () => {
const firstRow = operationLogPage.table.locator('.el-table__row').first();
await expect(firstRow).toBeVisible();
});
});
test('AUDIT-009: 验证登录日志状态显示', async ({ page }) => {
await test.step('管理员登录并导航到登录日志', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await loginLogPage.goto();
});
await test.step('验证登录状态列显示', async () => {
await expect(loginLogPage.table).toContainText('成功');
});
});
test('AUDIT-010: 验证审计日志数据完整性', async ({ page }) => {
await test.step('管理员登录并导航到操作日志', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await operationLogPage.goto();
});
await test.step('验证操作日志包含完整信息', async () => {
await expect(operationLogPage.table).toContainText('操作时间');
await expect(operationLogPage.table).toContainText('请求参数');
await expect(operationLogPage.table).toContainText('返回结果');
});
});
});
+16
View File
@@ -0,0 +1,16 @@
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
await page.locator('input[placeholder*="用户名"]').fill('admin');
await page.locator('input[placeholder*="密码"]').fill('Test@123');
await page.locator('button:has-text("登录")').click();
await page.waitForURL('**/dashboard', { timeout: 30000 });
await page.context().storageState({ path: authFile });
});
-68
View File
@@ -1,68 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
test.describe('用户认证 E2E 测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
await loginPage.goto();
});
test('成功登录流程', async ({ page }) => {
await expect(page).toHaveTitle(/登录/);
await loginPage.login('e2e_test_user', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
const username = await dashboardPage.getUsername();
expect(username).toContain('e2e_test_user');
});
test('登录失败 - 无效凭证', async ({ page }) => {
await loginPage.login('invalid', 'invalid');
await page.waitForTimeout(2000);
await expect(page).not.toHaveURL(/.*dashboard/);
const currentUrl = page.url();
expect(currentUrl).toContain('/login');
});
test('登录失败 - 缺少必填字段', async ({ page }) => {
await loginPage.usernameInput.fill('admin');
await loginPage.loginButton.click();
const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).toBeTruthy();
});
test('登出流程', async ({ page }) => {
await loginPage.login('admin', 'admin123');
await loginPage.logout();
await expect(page).toHaveURL(/.*login/);
await expect(page).toHaveTitle(/登录/);
});
test('登录后可以访问主要菜单', async ({ page }) => {
await loginPage.login('admin', 'admin123');
await dashboardPage.navigateToUserManagement();
await expect(page).toHaveURL(/.*users/);
await dashboardPage.navigateToRoleManagement();
await expect(page).toHaveURL(/.*roles/);
await dashboardPage.navigateToMenuManagement();
await expect(page).toHaveURL(/.*menus/);
await dashboardPage.navigateToSystemConfig();
await expect(page).toHaveURL(/.*sysconfig/);
});
});
-27
View File
@@ -1,27 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('基础功能测试', () => {
test('后端健康检查', async ({ request }) => {
const response = await request.get('http://localhost:8084/actuator/health');
expect(response.ok()).toBeTruthy();
const health = await response.json();
expect(health.status).toBe('UP');
});
test('前端首页加载', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/.*login.*/);
});
test('登录页面可访问', async ({ page }) => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
await expect(page.locator('h2')).toContainText('登录');
await expect(page.locator('input[placeholder*="用户名"]')).toBeVisible();
await expect(page.locator('input[placeholder*="密码"]')).toBeVisible();
});
});
@@ -1,270 +0,0 @@
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('完整业务流程 E2E 测试', () => {
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 timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建新角色', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.clickCreateRole();
const roleData = {
roleName: `测试角色_${timestamp}`,
roleKey: `test_role_${timestamp}`,
roleSort: '1',
status: '1',
remark: `测试角色备注_${timestamp}`,
};
await roleManagementPage.fillRoleForm(roleData);
await roleManagementPage.submitForm();
await expect(roleManagementPage.successMessage).toBeVisible();
await expect(roleManagementPage.table).toContainText(roleData.roleName);
});
await test.step('3. 为角色分配权限', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.openPermissionDialog(1);
await roleManagementPage.selectPermission('user:view');
await roleManagementPage.selectPermission('user:create');
await roleManagementPage.selectPermission('user:edit');
await roleManagementPage.savePermissions();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('4. 创建新用户', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
const userData = {
username: `testuser_${timestamp}`,
email: `test_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
await expect(userManagementPage.table).toContainText(userData.username);
});
await test.step('5. 为用户分配角色', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.editUser(1);
await page.click('.role-select');
await page.click('option:has-text("测试角色")');
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('6. 验证用户登录', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login(`testuser_${timestamp}`, 'Test123!@#');
await expect(page).toHaveURL(/.*dashboard/);
const username = await dashboardPage.getUsername();
expect(username).toContain(`testuser_${timestamp}`);
});
await test.step('7. 管理员删除测试用户', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await dashboardPage.navigateToUserManagement();
await userManagementPage.search(`testuser_${timestamp}`);
await userManagementPage.deleteUser(1);
await userManagementPage.confirmDelete();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('8. 管理员删除测试角色', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.search(`测试角色_${timestamp}`);
await roleManagementPage.deleteRole(1);
await roleManagementPage.confirmDelete();
await expect(roleManagementPage.successMessage).toBeVisible();
});
});
test('完整菜单管理流程:创建菜单 -> 构建菜单树 -> 删除菜单', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建父级菜单', async () => {
await dashboardPage.navigateToMenuManagement();
await page.click('text=新增菜单');
await page.fill('input[name="menuName"]', `父级菜单_${timestamp}`);
await page.fill('input[name="parentId"]', '0');
await page.fill('input[name="orderNum"]', '1');
await page.selectOption('select[name="menuType"]', 'M');
await page.fill('input[name="component"]', `parent_${timestamp}`);
await page.fill('input[name="perms"]', `parent:view_${timestamp}`);
await page.selectOption('select[name="status"]', '1');
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
});
await test.step('3. 创建子级菜单', async () => {
await dashboardPage.navigateToMenuManagement();
await page.click('text=新增菜单');
await page.fill('input[name="menuName"]', `子级菜单_${timestamp}`);
await page.fill('input[name="parentId"]', '1');
await page.fill('input[name="orderNum"]', '1');
await page.selectOption('select[name="menuType"]', 'C');
await page.fill('input[name="component"]', `child_${timestamp}`);
await page.fill('input[name="perms"]', `child:view_${timestamp}`);
await page.selectOption('select[name="status"]', '1');
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
});
await test.step('4. 验证菜单树结构', async () => {
await dashboardPage.navigateToMenuManagement();
await expect(page.locator('table')).toContainText(`父级菜单_${timestamp}`);
await expect(page.locator('table')).toContainText(`子级菜单_${timestamp}`);
});
await test.step('5. 删除子级菜单', async () => {
await dashboardPage.navigateToMenuManagement();
await page.click('table tbody tr:has-text("子级菜单") .delete-button');
await page.click('.confirm-dialog .confirm-button');
await expect(page.locator('.success-message')).toBeVisible();
});
await test.step('6. 删除父级菜单', async () => {
await dashboardPage.navigateToMenuManagement();
await page.click('table tbody tr:has-text("父级菜单") .delete-button');
await page.click('.confirm-dialog .confirm-button');
await expect(page.locator('.success-message')).toBeVisible();
});
});
test('完整系统配置流程:修改配置 -> 验证配置 -> 恢复默认', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 修改系统配置', async () => {
await dashboardPage.navigateToSystemConfig();
await page.click('table tbody tr:first-child .edit-button');
await page.fill('input[name="configValue"]', `test_value_${timestamp}`);
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
});
await test.step('3. 验证配置修改', async () => {
await dashboardPage.navigateToSystemConfig();
await expect(page.locator('table')).toContainText(`test_value_${timestamp}`);
});
await test.step('4. 恢复默认配置', async () => {
await dashboardPage.navigateToSystemConfig();
await page.click('table tbody tr:first-child .edit-button');
await page.fill('input[name="configValue"]', 'default_value');
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
});
});
test('完整权限控制流程:创建受限角色 -> 创建用户 -> 验证权限限制', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建受限角色', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.clickCreateRole();
const roleData = {
roleName: `受限角色_${timestamp}`,
roleKey: `limited_role_${timestamp}`,
roleSort: '1',
status: '1',
remark: '仅查看权限',
};
await roleManagementPage.fillRoleForm(roleData);
await roleManagementPage.submitForm();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('3. 为受限角色分配仅查看权限', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.openPermissionDialog(1);
await roleManagementPage.selectPermission('user:view');
await roleManagementPage.savePermissions();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('4. 创建受限用户', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
const userData = {
username: `limiteduser_${timestamp}`,
email: `limited_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('5. 验证受限用户权限', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login(`limiteduser_${timestamp}`, 'Test123!@#');
await expect(page).toHaveURL(/.*dashboard/);
await dashboardPage.navigateToUserManagement();
await expect(page).toHaveURL(/.*users/);
await page.goto('/users/create');
await expect(page).toHaveURL(/.*dashboard/);
});
});
});
@@ -1,773 +0,0 @@
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 { MenuManagementPage } from './pages/MenuManagementPage';
import { SystemConfigPage } from './pages/SystemConfigPage';
import { FileManagementPage } from './pages/FileManagementPage';
import { OperationLogPage } from './pages/OperationLogPage';
import { NotificationPage } from './pages/NotificationPage';
import { DictionaryManagementPage } from './pages/DictionaryManagementPage';
test.describe('E2E完整业务流程测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let userManagementPage: UserManagementPage;
let roleManagementPage: RoleManagementPage;
let menuManagementPage: MenuManagementPage;
let systemConfigPage: SystemConfigPage;
let fileManagementPage: FileManagementPage;
let operationLogPage: OperationLogPage;
let notificationPage: NotificationPage;
let dictionaryManagementPage: DictionaryManagementPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
userManagementPage = new UserManagementPage(page);
roleManagementPage = new RoleManagementPage(page);
menuManagementPage = new MenuManagementPage(page);
systemConfigPage = new SystemConfigPage(page);
fileManagementPage = new FileManagementPage(page);
operationLogPage = new OperationLogPage(page);
notificationPage = new NotificationPage(page);
dictionaryManagementPage = new DictionaryManagementPage(page);
});
test('E2E-001: 用户完整生命周期流程', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建新角色', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.clickCreateRole();
const roleData = {
roleName: `测试角色_${timestamp}`,
roleKey: `test_role_${timestamp}`,
roleSort: '1',
status: 'ACTIVE',
remark: `测试角色备注_${timestamp}`,
};
await roleManagementPage.fillRoleForm(roleData);
await roleManagementPage.submitForm();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('3. 为角色分配权限', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.openPermissionDialog(1);
await roleManagementPage.selectPermission('user:view');
await roleManagementPage.selectPermission('user:create');
await roleManagementPage.selectPermission('user:edit');
await roleManagementPage.selectPermission('user:delete');
await roleManagementPage.savePermissions();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('4. 创建新用户', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
const userData = {
username: `testuser_${timestamp}`,
nickname: `测试用户${timestamp}`,
email: `test_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('5. 为用户分配角色', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.editUser(1);
await page.click('.role-select');
await page.click('option:has-text("测试角色")');
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('6. 用户登录验证', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login(`testuser_${timestamp}`, 'Test123!@#');
await expect(page).toHaveURL(/.*dashboard/);
const username = await dashboardPage.getUsername();
expect(username).toContain(`testuser_${timestamp}`);
});
await test.step('7. 修改用户信息', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await dashboardPage.navigateToUserManagement();
await userManagementPage.editUser(1);
const dialog = page.locator('.el-dialog');
const nicknameInput = dialog.locator('.el-form-item').filter({ hasText: '昵称' }).locator('input');
await nicknameInput.fill(`更新用户_${timestamp}`);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('8. 禁用用户', async () => {
await userManagementPage.clickStatusButton(1);
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('9. 启用用户', async () => {
await userManagementPage.clickStatusButton(1);
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('10. 删除用户', async () => {
await userManagementPage.deleteUser(1);
await userManagementPage.confirmDelete();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('11. 删除角色', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.deleteRole(1);
await roleManagementPage.confirmDelete();
await expect(roleManagementPage.successMessage).toBeVisible();
});
});
test('E2E-002: 角色权限分配完整流程', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建新角色', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.clickCreateRole();
const roleData = {
roleName: `UAT角色_${timestamp}`,
roleKey: `uat_role_${timestamp}`,
roleSort: '1',
status: '1',
remark: 'UAT测试角色',
};
await roleManagementPage.fillRoleForm(roleData);
await roleManagementPage.submitForm();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('3. 为角色分配菜单权限', async () => {
await roleManagementPage.openPermissionDialog(1);
await roleManagementPage.selectPermission('system:user:view');
await roleManagementPage.selectPermission('system:user:add');
await roleManagementPage.submitPermissions();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('4. 为角色分配API权限', async () => {
await roleManagementPage.openPermissionDialog(1);
await roleManagementPage.selectPermission('api:user:list');
await roleManagementPage.selectPermission('api:user:create');
await roleManagementPage.submitPermissions();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('5. 创建新用户', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
const userData = {
username: `uatuser_${timestamp}`,
nickname: `UAT用户${timestamp}`,
email: `uat_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('6. 为用户分配角色', async () => {
await userManagementPage.editUser(1);
await page.click('.role-select');
await page.click('option:has-text("UAT角色")');
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('7. 用户登录验证权限', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login(`uatuser_${timestamp}`, 'Test123!@#');
await expect(page).toHaveURL(/.*dashboard/);
await dashboardPage.navigateToUserManagement();
await expect(page).toHaveURL(/.*users/);
await page.goto('/users/create');
await expect(page).toHaveURL(/.*users/);
});
await test.step('8. 撤销角色权限', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.openPermissionDialog(1);
await roleManagementPage.deselectPermission('system:user:add');
await roleManagementPage.submitPermissions();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('9. 删除角色', async () => {
await roleManagementPage.deleteRole(1);
await roleManagementPage.confirmDelete();
await expect(roleManagementPage.successMessage).toBeVisible();
});
});
test('E2E-003: 菜单树构建与权限控制流程', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建父级菜单', async () => {
await dashboardPage.navigateToMenuManagement();
await menuManagementPage.clickCreateMenu();
const menuData = {
menuName: `父级菜单_${timestamp}`,
parentId: '0',
orderNum: '1',
menuType: 'M',
component: `parent_${timestamp}`,
perms: `parent:view_${timestamp}`,
status: '1',
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.successMessage).toBeVisible();
});
await test.step('3. 创建子级菜单', async () => {
await menuManagementPage.clickCreateMenu();
const menuData = {
menuName: `子级菜单_${timestamp}`,
parentId: '1',
orderNum: '1',
menuType: 'C',
component: `child_${timestamp}`,
perms: `child:view_${timestamp}`,
status: '1',
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.successMessage).toBeVisible();
});
await test.step('4. 配置菜单权限', async () => {
await menuManagementPage.editMenu(1);
await menuManagementPage.selectPermission('menu:view');
await menuManagementPage.submitForm();
await expect(menuManagementPage.successMessage).toBeVisible();
});
await test.step('5. 验证菜单树显示', async () => {
await page.reload();
await expect(page.locator('table')).toContainText(`父级菜单_${timestamp}`);
await expect(page.locator('table')).toContainText(`子级菜单_${timestamp}`);
});
await test.step('6. 为角色分配菜单权限', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.openPermissionDialog(1);
await roleManagementPage.selectPermission(`parent:view_${timestamp}`);
await roleManagementPage.submitPermissions();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('7. 用户登录验证菜单访问', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('.menu-item')).toContainText(`父级菜单_${timestamp}`);
});
await test.step('8. 删除子级菜单', async () => {
await dashboardPage.navigateToMenuManagement();
await menuManagementPage.deleteMenu(2);
await menuManagementPage.confirmDelete();
await expect(menuManagementPage.successMessage).toBeVisible();
});
await test.step('9. 删除父级菜单', async () => {
await menuManagementPage.deleteMenu(1);
await menuManagementPage.confirmDelete();
await expect(menuManagementPage.successMessage).toBeVisible();
});
});
test('E2E-004: 系统配置管理流程', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 查看当前配置', async () => {
await dashboardPage.navigateToSystemConfig();
await expect(systemConfigPage.table).toBeVisible();
});
await test.step('3. 修改配置值', async () => {
await systemConfigPage.editConfig(1);
await page.fill('input[name="configValue"]', `test_value_${timestamp}`);
await systemConfigPage.submitForm();
await expect(systemConfigPage.successMessage).toBeVisible();
});
await test.step('4. 验证配置生效', async () => {
await page.reload();
await expect(page.locator('table')).toContainText(`test_value_${timestamp}`);
});
await test.step('5. 刷新配置缓存', async () => {
await systemConfigPage.refreshCache();
await expect(systemConfigPage.successMessage).toBeVisible();
});
await test.step('6. 恢复默认配置', async () => {
await systemConfigPage.editConfig(1);
await page.fill('input[name="configValue"]', 'default_value');
await systemConfigPage.submitForm();
await expect(systemConfigPage.successMessage).toBeVisible();
});
await test.step('7. 批量修改配置', async () => {
await systemConfigPage.editConfig(2);
await page.fill('input[name="configValue"]', `batch_value_${timestamp}`);
await systemConfigPage.submitForm();
await expect(systemConfigPage.successMessage).toBeVisible();
});
});
test('E2E-005: 文件管理完整流程', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 上传文件', async () => {
await dashboardPage.navigateToFileManagement();
await fileManagementPage.clickUploadFile();
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles('./e2e/fixtures/test-file.txt');
await fileManagementPage.submitUpload();
await expect(fileManagementPage.successMessage).toBeVisible();
});
await test.step('3. 验证文件信息', async () => {
await expect(page.locator('table')).toContainText('test-file.txt');
});
await test.step('4. 预览文件', async () => {
await fileManagementPage.previewFile(1);
await expect(page.locator('.file-preview')).toBeVisible();
});
await test.step('5. 下载文件', async () => {
const downloadPromise = page.waitForEvent('download');
await fileManagementPage.downloadFile(1);
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('test-file.txt');
});
await test.step('6. 设置文件权限', async () => {
await fileManagementPage.editFile(1);
await page.selectOption('select[name="permission"]', 'private');
await fileManagementPage.submitForm();
await expect(fileManagementPage.successMessage).toBeVisible();
});
await test.step('7. 删除文件', async () => {
await fileManagementPage.deleteFile(1);
await fileManagementPage.confirmDelete();
await expect(fileManagementPage.successMessage).toBeVisible();
});
});
test('E2E-006: 审计日志记录与查询流程', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 执行各种操作', async () => {
await dashboardPage.navigateToUserManagement();
await page.waitForTimeout(1000);
await dashboardPage.navigateToRoleManagement();
await page.waitForTimeout(1000);
await dashboardPage.navigateToMenuManagement();
await page.waitForTimeout(1000);
});
await test.step('3. 查看操作日志', async () => {
await dashboardPage.navigateToOperationLog();
await expect(operationLogPage.table).toBeVisible();
await expect(page.locator('table')).toContainText('用户管理');
});
await test.step('4. 查看登录日志', async () => {
await operationLogPage.switchToLoginLog();
await expect(page.locator('table')).toContainText('admin');
});
await test.step('5. 查看异常日志', async () => {
await operationLogPage.switchToExceptionLog();
await expect(operationLogPage.table).toBeVisible();
});
await test.step('6. 搜索日志', async () => {
await operationLogPage.search('用户管理');
await page.waitForTimeout(2000);
await expect(page.locator('table')).toContainText('用户管理');
});
await test.step('7. 导出日志', async () => {
const downloadPromise = page.waitForEvent('download');
await operationLogPage.exportLogs();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/logs.*\.xlsx/);
});
});
test('E2E-007: 通知发布与推送流程', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 发布系统通知', async () => {
await dashboardPage.navigateToNotification();
await notificationPage.clickCreateNotification();
const notificationData = {
title: `系统通知_${timestamp}`,
content: `这是一条测试通知内容_${timestamp}`,
type: 'system',
status: '1',
};
await notificationPage.fillNotificationForm(notificationData);
await notificationPage.submitForm();
await expect(notificationPage.successMessage).toBeVisible();
});
await test.step('3. 发布用户消息', async () => {
await notificationPage.clickCreateNotification();
const notificationData = {
title: `用户消息_${timestamp}`,
content: `这是一条测试用户消息_${timestamp}`,
type: 'user',
status: '1',
};
await notificationPage.fillNotificationForm(notificationData);
await notificationPage.submitForm();
await expect(notificationPage.successMessage).toBeVisible();
});
await test.step('4. 推送实时消息', async () => {
await notificationPage.pushRealTimeMessage(1);
await expect(notificationPage.successMessage).toBeVisible();
});
await test.step('5. 用户查看通知', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('.notification-badge')).toBeVisible();
});
await test.step('6. 标记通知已读', async () => {
await dashboardPage.navigateToNotification();
await notificationPage.markAsRead(1);
await expect(notificationPage.successMessage).toBeVisible();
});
await test.step('7. 删除通知', async () => {
await notificationPage.deleteNotification(1);
await notificationPage.confirmDelete();
await expect(notificationPage.successMessage).toBeVisible();
});
});
test('E2E-008: 字典数据管理流程', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建字典类型', async () => {
await dashboardPage.navigateToDictionary();
await dictionaryManagementPage.clickCreateDictType();
const dictTypeData = {
dictName: `测试字典_${timestamp}`,
dictType: `test_dict_${timestamp}`,
status: '1',
remark: `测试字典类型_${timestamp}`,
};
await dictionaryManagementPage.fillDictTypeForm(dictTypeData);
await dictionaryManagementPage.submitForm();
await expect(dictionaryManagementPage.successMessage).toBeVisible();
});
await test.step('3. 添加字典数据', async () => {
await dictionaryManagementPage.clickCreateDictData();
const dictData = {
dictLabel: `测试数据1_${timestamp}`,
dictValue: `value1_${timestamp}`,
dictSort: '1',
status: '1',
};
await dictionaryManagementPage.fillDictDataForm(dictData);
await dictionaryManagementPage.submitForm();
await expect(dictionaryManagementPage.successMessage).toBeVisible();
});
await test.step('4. 修改字典数据', async () => {
await dictionaryManagementPage.editDictData(1);
await page.fill('input[name="dictLabel"]', `更新数据_${timestamp}`);
await dictionaryManagementPage.submitForm();
await expect(dictionaryManagementPage.successMessage).toBeVisible();
});
await test.step('5. 查询字典数据', async () => {
await dictionaryManagementPage.search(`更新数据_${timestamp}`);
await page.waitForTimeout(2000);
await expect(page.locator('table')).toContainText(`更新数据_${timestamp}`);
});
await test.step('6. 删除字典数据', async () => {
await dictionaryManagementPage.deleteDictData(1);
await dictionaryManagementPage.confirmDelete();
await expect(dictionaryManagementPage.successMessage).toBeVisible();
});
await test.step('7. 删除字典类型', async () => {
await dictionaryManagementPage.deleteDictType(1);
await dictionaryManagementPage.confirmDelete();
await expect(dictionaryManagementPage.successMessage).toBeVisible();
});
});
test('E2E-009: 多用户并发操作流程', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 创建测试用户', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await dashboardPage.navigateToUserManagement();
for (let i = 1; i <= 2; i++) {
await userManagementPage.clickCreateUser();
const userData = {
username: `concurrent_user_${i}_${timestamp}`,
nickname: `并发用户${i}_${timestamp}`,
email: `concurrent_${i}_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
}
});
await test.step('2. 用户A创建数据', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login(`concurrent_user_1_${timestamp}`, 'Test123!@#');
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
const userData = {
username: `user_a_data_${timestamp}`,
nickname: `用户A数据_${timestamp}`,
email: `user_a_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('3. 用户B同时创建数据', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login(`concurrent_user_2_${timestamp}`, 'Test123!@#');
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
const userData = {
username: `user_b_data_${timestamp}`,
nickname: `用户B数据_${timestamp}`,
email: `user_b_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('4. 验证数据一致性', async () => {
await page.reload();
await expect(page.locator('table')).toContainText(`user_a_data_${timestamp}`);
await expect(page.locator('table')).toContainText(`user_b_data_${timestamp}`);
});
await test.step('5. 清理测试数据', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await dashboardPage.navigateToUserManagement();
await userManagementPage.search(`concurrent_user_1_${timestamp}`);
await page.waitForTimeout(1000);
const rows = await page.locator('table tbody tr').count();
if (rows > 0) {
await userManagementPage.deleteUser(1);
await userManagementPage.confirmDelete();
}
await userManagementPage.search(`concurrent_user_2_${timestamp}`);
await page.waitForTimeout(1000);
const rows2 = await page.locator('table tbody tr').count();
if (rows2 > 0) {
await userManagementPage.deleteUser(1);
await userManagementPage.confirmDelete();
}
});
});
test('E2E-010: 系统异常恢复流程', async ({ page }) => {
const timestamp = Date.now();
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建测试数据', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
const userData = {
username: `recovery_test_${timestamp}`,
nickname: `恢复测试用户_${timestamp}`,
email: `recovery_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('3. 记录数据状态', async () => {
await page.reload();
await expect(page.locator('table')).toContainText(`recovery_test_${timestamp}`);
});
await test.step('4. 模拟网络中断', async () => {
await page.context().setOffline(true);
await page.waitForTimeout(2000);
});
await test.step('5. 恢复网络连接', async () => {
await page.context().setOffline(false);
await page.waitForTimeout(2000);
});
await test.step('6. 验证数据完整性', async () => {
await page.reload();
await expect(page.locator('table')).toContainText(`recovery_test_${timestamp}`);
});
await test.step('7. 验证会话恢复', async () => {
await expect(page).toHaveURL(/.*dashboard/);
const username = await dashboardPage.getUsername();
expect(username).toContain('admin');
});
await test.step('8. 验证操作继续', async () => {
await dashboardPage.navigateToUserManagement();
await expect(page).toHaveURL(/.*users/);
await expect(page.locator('table')).toBeVisible();
});
await test.step('9. 清理测试数据', async () => {
await userManagementPage.search(`recovery_test_${timestamp}`);
await page.waitForTimeout(1000);
const rows = await page.locator('table tbody tr').count();
if (rows > 0) {
await userManagementPage.deleteUser(1);
await userManagementPage.confirmDelete();
await expect(userManagementPage.successMessage).toBeVisible();
}
});
});
});
@@ -1,833 +0,0 @@
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 { MenuManagementPage } from './pages/MenuManagementPage';
import { SystemConfigPage } from './pages/SystemConfigPage';
import { FileManagementPage } from './pages/FileManagementPage';
import { OperationLogPage } from './pages/OperationLogPage';
import { NotificationPage } from './pages/NotificationPage';
import { DictionaryManagementPage } from './pages/DictionaryManagementPage';
import { TestDataCleanup } from './utils/TestDataCleanup';
test.describe('UAT用户验收测试', () => {
let testDataCleanup: TestDataCleanup;
test.beforeEach(async ({ page }) => {
testDataCleanup = new TestDataCleanup(page);
});
test.afterEach(async ({ page }) => {
await testDataCleanup.cleanupAll();
});
test('UAT-001: 用户注册与首次登录场景', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const userManagementPage = new UserManagementPage(page);
const timestamp = Date.now();
await test.step('1. 管理员登录系统', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建新用户账号', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
const userData = {
username: `newuser_${timestamp}`,
nickname: `新员工${timestamp}`,
email: `newuser_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
testDataCleanup.trackUser(userData.username);
});
await test.step('3. 设置初始密码', async () => {
await userManagementPage.editUser(1);
const dialog = page.locator('.el-dialog');
const passwordInput = dialog.locator('.el-form-item').filter({ hasText: '密码' }).locator('input[type="password"]');
await passwordInput.fill('NewPass123!@#');
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('4. 分配基本角色', async () => {
await userManagementPage.editUser(1);
await page.click('.role-select');
await page.click('option:has-text("普通用户")');
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('5. 新用户使用初始密码登录', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login(`newuser_${timestamp}`, 'NewPass123!@#');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('6. 验证密码修改提示', async () => {
await expect(page.locator('.password-change-notice')).toBeVisible();
});
await test.step('7. 修改密码', async () => {
await dashboardPage.navigateToProfile();
await page.fill('input[name="oldPassword"]', 'NewPass123!@#');
await page.fill('input[name="newPassword"]', 'FinalPass123!@#');
await page.fill('input[name="confirmPassword"]', 'FinalPass123!@#');
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
});
await test.step('8. 验证登录成功', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login(`newuser_${timestamp}`, 'FinalPass123!@#');
await expect(page).toHaveURL(/.*dashboard/);
const username = await dashboardPage.getUsername();
expect(username).toContain(`newuser_${timestamp}`);
});
await test.step('9. 查看欢迎信息', async () => {
await expect(page.locator('.welcome-message')).toBeVisible();
await expect(page.locator('.welcome-message')).toContainText('欢迎');
});
await test.step('10. 查看系统通知', async () => {
await dashboardPage.navigateToNotification();
await expect(page.locator('.notification-list')).toBeVisible();
});
});
test('UAT-002: 用户信息管理场景', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const userManagementPage = new UserManagementPage(page);
const timestamp = Date.now();
await test.step('1. 用户登录系统', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 导航到个人信息页面', async () => {
await dashboardPage.navigateToProfile();
await expect(page.locator('.profile-form')).toBeVisible();
});
await test.step('3. 查看当前信息', async () => {
const currentUsername = await page.locator('input[name="username"]').inputValue();
expect(currentUsername).toBe('admin');
});
await test.step('4. 修改昵称', async () => {
await page.fill('input[name="nickname"]', `管理员_${timestamp}`);
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
});
await test.step('5. 修改邮箱', async () => {
await page.fill('input[name="email"]', `admin_${timestamp}@example.com`);
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
});
await test.step('6. 修改手机号', async () => {
await page.fill('input[name="phone"]', '13900139000');
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
});
await test.step('7. 上传头像', async () => {
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles('./e2e/fixtures/test-file.txt');
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
});
await test.step('8. 保存修改', async () => {
await page.click('button:has-text("保存")');
await expect(page.locator('.success-message')).toBeVisible();
});
await test.step('9. 验证信息更新', async () => {
await page.reload();
await expect(page.locator('input[name="nickname"]')).toHaveValue(`管理员_${timestamp}`);
});
await test.step('10. 查看操作日志', async () => {
await dashboardPage.navigateToOperationLog();
await expect(page.locator('table')).toContainText('个人信息');
});
});
test('UAT-003: 角色权限分配场景', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const roleManagementPage = new RoleManagementPage(page);
const userManagementPage = new UserManagementPage(page);
const timestamp = Date.now();
await test.step('1. 管理员登录系统', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建新角色', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.clickCreateRole();
const roleData = {
roleName: `业务角色_${timestamp}`,
roleKey: `business_role_${timestamp}`,
roleSort: '1',
status: '1',
remark: `业务操作角色_${timestamp}`,
};
await roleManagementPage.fillRoleForm(roleData);
await roleManagementPage.submitForm();
await expect(roleManagementPage.successMessage).toBeVisible();
testDataCleanup.trackRole(roleData.roleKey);
});
await test.step('3. 配置角色基本信息', async () => {
await roleManagementPage.editRole(1);
await page.fill('input[name="remark"]', `更新备注_${timestamp}`);
await roleManagementPage.submitForm();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('4. 分配菜单权限', async () => {
await roleManagementPage.openPermissionDialog(1);
await roleManagementPage.selectPermission('system:user:view');
await roleManagementPage.selectPermission('system:user:add');
await roleManagementPage.selectPermission('system:user:edit');
await roleManagementPage.submitPermissions();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('5. 分配API权限', async () => {
await roleManagementPage.openPermissionDialog(1);
await roleManagementPage.selectPermission('api:user:list');
await roleManagementPage.selectPermission('api:user:create');
await roleManagementPage.submitPermissions();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('6. 保存角色配置', async () => {
await roleManagementPage.saveRole();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('7. 为用户分配角色', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.editUser(1);
await page.click('.role-select');
await page.click('option:has-text("业务角色")');
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('8. 用户重新登录', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('9. 验证权限生效', async () => {
await dashboardPage.navigateToUserManagement();
await expect(page).toHaveURL(/.*users/);
await expect(page.locator('button:has-text("新增")')).toBeVisible();
});
await test.step('10. 查看权限日志', async () => {
await dashboardPage.navigateToOperationLog();
await expect(page.locator('table')).toContainText('权限');
});
});
test('UAT-004: 菜单管理场景', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const menuManagementPage = new MenuManagementPage(page);
const roleManagementPage = new RoleManagementPage(page);
const timestamp = Date.now();
await test.step('1. 管理员登录系统', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 创建父级菜单', async () => {
await dashboardPage.navigateToMenuManagement();
await menuManagementPage.clickCreateMenu();
const menuData = {
menuName: `业务菜单_${timestamp}`,
parentId: '0',
orderNum: '1',
menuType: 'M',
component: `business_${timestamp}`,
perms: `business:view_${timestamp}`,
status: '1',
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.successMessage).toBeVisible();
testDataCleanup.trackMenu(`business_${timestamp}`);
});
await test.step('3. 创建子级菜单', async () => {
await menuManagementPage.clickCreateMenu();
const menuData = {
menuName: `业务操作_${timestamp}`,
parentId: '1',
orderNum: '1',
menuType: 'C',
component: `business_operation_${timestamp}`,
perms: `business:operation_${timestamp}`,
status: '1',
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.successMessage).toBeVisible();
});
await test.step('4. 配置菜单权限', async () => {
await menuManagementPage.editMenu(1);
await menuManagementPage.selectPermission('menu:view');
await menuManagementPage.selectPermission('menu:edit');
await menuManagementPage.submitForm();
await expect(menuManagementPage.successMessage).toBeVisible();
});
await test.step('5. 保存菜单配置', async () => {
await menuManagementPage.saveMenu();
await expect(menuManagementPage.successMessage).toBeVisible();
});
await test.step('6. 为角色分配菜单权限', async () => {
await dashboardPage.navigateToRoleManagement();
await roleManagementPage.openPermissionDialog(1);
await roleManagementPage.selectPermission(`business:view_${timestamp}`);
await roleManagementPage.submitPermissions();
await expect(roleManagementPage.successMessage).toBeVisible();
});
await test.step('7. 用户登录系统', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('8. 验证菜单显示', async () => {
await expect(page.locator('.menu-item')).toContainText(`业务菜单_${timestamp}`);
});
await test.step('9. 验证菜单访问', async () => {
await page.click(`text=业务菜单_${timestamp}`);
await expect(page).toHaveURL(/.*business/);
});
await test.step('10. 验证菜单结构', async () => {
await dashboardPage.navigateToMenuManagement();
await expect(page.locator('table')).toContainText(`业务菜单_${timestamp}`);
await expect(page.locator('table')).toContainText(`业务操作_${timestamp}`);
});
});
test('UAT-005: 文件管理场景', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const fileManagementPage = new FileManagementPage(page);
const timestamp = Date.now();
await test.step('1. 用户登录系统', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 导航到文件管理页面', async () => {
await dashboardPage.navigateToFileManagement();
await expect(fileManagementPage.table).toBeVisible();
});
await test.step('3. 上传文件', async () => {
await fileManagementPage.clickUploadFile();
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles('./e2e/fixtures/test-file.txt');
await fileManagementPage.submitUpload();
await expect(fileManagementPage.successMessage).toBeVisible();
});
await test.step('4. 验证文件上传成功', async () => {
await expect(page.locator('table')).toContainText('test-file.txt');
});
await test.step('5. 预览文件', async () => {
await fileManagementPage.previewFile(1);
await expect(page.locator('.file-preview')).toBeVisible();
await expect(page.locator('.file-preview')).toContainText('test');
});
await test.step('6. 下载文件', async () => {
const downloadPromise = page.waitForEvent('download');
await fileManagementPage.downloadFile(1);
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('test-file.txt');
});
await test.step('7. 验证文件内容', async () => {
await fileManagementPage.previewFile(1);
const content = await page.locator('.file-preview').textContent();
expect(content).toContain('test');
});
await test.step('8. 设置文件权限', async () => {
await fileManagementPage.editFile(1);
await page.selectOption('select[name="permission"]', 'private');
await fileManagementPage.submitForm();
await expect(fileManagementPage.successMessage).toBeVisible();
});
await test.step('9. 删除文件', async () => {
await fileManagementPage.deleteFile(1);
await fileManagementPage.confirmDelete();
await expect(fileManagementPage.successMessage).toBeVisible();
});
await test.step('10. 验证文件删除', async () => {
await page.reload();
await expect(page.locator('table')).not.toContainText('test-file.txt');
});
});
test('UAT-006: 系统配置管理场景', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const systemConfigPage = new SystemConfigPage(page);
const timestamp = Date.now();
await test.step('1. 管理员登录系统', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 导航到系统配置页面', async () => {
await dashboardPage.navigateToSystemConfig();
await expect(systemConfigPage.table).toBeVisible();
});
await test.step('3. 查看当前配置', async () => {
const configCount = await page.locator('table tbody tr').count();
expect(configCount).toBeGreaterThan(0);
});
await test.step('4. 修改配置项', async () => {
await systemConfigPage.editConfig(1);
await page.fill('input[name="configValue"]', `test_config_${timestamp}`);
await systemConfigPage.submitForm();
await expect(systemConfigPage.successMessage).toBeVisible();
});
await test.step('5. 验证配置有效性', async () => {
await systemConfigPage.editConfig(1);
await expect(page.locator('input[name="configValue"]')).toHaveValue(`test_config_${timestamp}`);
});
await test.step('6. 保存配置', async () => {
await systemConfigPage.submitForm();
await expect(systemConfigPage.successMessage).toBeVisible();
});
await test.step('7. 验证配置生效', async () => {
await page.reload();
await expect(page.locator('table')).toContainText(`test_config_${timestamp}`);
});
await test.step('8. 刷新配置缓存', async () => {
await systemConfigPage.refreshCache();
await expect(systemConfigPage.successMessage).toBeVisible();
});
await test.step('9. 查看配置日志', async () => {
await dashboardPage.navigateToOperationLog();
await expect(page.locator('table')).toContainText('配置');
});
await test.step('10. 恢复默认配置', async () => {
await dashboardPage.navigateToSystemConfig();
await systemConfigPage.editConfig(1);
await page.fill('input[name="configValue"]', 'default_value');
await systemConfigPage.submitForm();
await expect(systemConfigPage.successMessage).toBeVisible();
});
});
test('UAT-007: 审计日志查询场景', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const operationLogPage = new OperationLogPage(page);
await test.step('1. 审计员登录系统', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('2. 导航到审计日志页面', async () => {
await dashboardPage.navigateToOperationLog();
await expect(operationLogPage.table).toBeVisible();
});
await test.step('3. 查看操作日志', async () => {
await expect(page.locator('table')).toContainText('操作');
});
await test.step('4. 查看登录日志', async () => {
await operationLogPage.switchToLoginLog();
await expect(page.locator('table')).toContainText('登录');
});
await test.step('5. 查看异常日志', async () => {
await operationLogPage.switchToExceptionLog();
await expect(operationLogPage.table).toBeVisible();
});
await test.step('6. 搜索日志', async () => {
await operationLogPage.search('admin');
await page.waitForTimeout(2000);
await expect(page.locator('table')).toContainText('admin');
});
await test.step('7. 导出日志', async () => {
const downloadPromise = page.waitForEvent('download');
await operationLogPage.exportLogs();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/logs.*\.xlsx/);
});
await test.step('8. 验证日志准确性', async () => {
const logCount = await page.locator('table tbody tr').count();
expect(logCount).toBeGreaterThan(0);
});
await test.step('9. 生成审计报告', async () => {
await operationLogPage.generateReport();
await expect(operationLogPage.successMessage).toBeVisible();
});
await test.step('10. 验证报告内容', async () => {
await expect(page.locator('.report-content')).toBeVisible();
});
});
test('UAT-008: 通知中心使用场景', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const notificationPage = new NotificationPage(page);
const timestamp = Date.now();
await test.step('1. 管理员发布系统通知', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await dashboardPage.navigateToNotification();
await notificationPage.clickCreateNotification();
const notificationData = {
title: `系统通知_${timestamp}`,
content: `这是一条重要的系统通知_${timestamp}`,
type: 'system',
status: '1',
};
await notificationPage.fillNotificationForm(notificationData);
await notificationPage.submitForm();
await expect(notificationPage.successMessage).toBeVisible();
});
await test.step('2. 用户登录系统', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('3. 查看通知列表', async () => {
await dashboardPage.navigateToNotification();
await expect(page.locator('.notification-list')).toBeVisible();
await expect(page.locator('.notification-list')).toContainText(`系统通知_${timestamp}`);
});
await test.step('4. 查看通知详情', async () => {
await notificationPage.viewNotification(1);
await expect(page.locator('.notification-detail')).toBeVisible();
await expect(page.locator('.notification-detail')).toContainText(`系统通知_${timestamp}`);
});
await test.step('5. 标记通知已读', async () => {
await notificationPage.markAsRead(1);
await expect(notificationPage.successMessage).toBeVisible();
});
await test.step('6. 验证通知状态', async () => {
await page.reload();
await expect(page.locator('.notification-item.read')).toBeVisible();
});
await test.step('7. 删除通知', async () => {
await notificationPage.deleteNotification(1);
await notificationPage.confirmDelete();
await expect(notificationPage.successMessage).toBeVisible();
});
await test.step('8. 验证通知删除', async () => {
await page.reload();
await expect(page.locator('.notification-list')).not.toContainText(`系统通知_${timestamp}`);
});
await test.step('9. 验证通知推送', async () => {
await notificationPage.clickCreateNotification();
const notificationData = {
title: `推送通知_${timestamp}`,
content: `这是一条推送通知_${timestamp}`,
type: 'push',
status: '1',
};
await notificationPage.fillNotificationForm(notificationData);
await notificationPage.submitForm();
await expect(notificationPage.successMessage).toBeVisible();
});
await test.step('10. 查看通知历史', async () => {
await page.reload();
await expect(page.locator('.notification-list')).toContainText(`推送通知_${timestamp}`);
});
});
test('UAT-009: 字典数据使用场景', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const dictionaryManagementPage = new DictionaryManagementPage(page);
const timestamp = Date.now();
await test.step('1. 管理员配置字典数据', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await dashboardPage.navigateToDictionary();
await dictionaryManagementPage.clickCreateDictType();
const dictTypeData = {
dictName: `业务字典_${timestamp}`,
dictType: `business_dict_${timestamp}`,
status: '1',
remark: `业务字典类型_${timestamp}`,
};
await dictionaryManagementPage.fillDictTypeForm(dictTypeData);
await dictionaryManagementPage.submitForm();
await expect(dictionaryManagementPage.successMessage).toBeVisible();
testDataCleanup.trackDictType(`business_dict_${timestamp}`);
});
await test.step('2. 用户登录系统', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('3. 查看字典数据', async () => {
await dashboardPage.navigateToDictionary();
await expect(page.locator('table')).toContainText(`业务字典_${timestamp}`);
});
await test.step('4. 使用字典数据', async () => {
await dictionaryManagementPage.clickCreateDictData();
const dictData = {
dictLabel: `业务数据1_${timestamp}`,
dictValue: `business_value1_${timestamp}`,
dictSort: '1',
status: '1',
};
await dictionaryManagementPage.fillDictDataForm(dictData);
await dictionaryManagementPage.submitForm();
await expect(dictionaryManagementPage.successMessage).toBeVisible();
});
await test.step('5. 验证数据正确性', async () => {
await page.reload();
await expect(page.locator('table')).toContainText(`业务数据1_${timestamp}`);
});
await test.step('6. 管理员更新字典数据', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await dashboardPage.navigateToDictionary();
await dictionaryManagementPage.editDictData(1);
await page.fill('input[name="dictLabel"]', `更新数据_${timestamp}`);
await dictionaryManagementPage.submitForm();
await expect(dictionaryManagementPage.successMessage).toBeVisible();
});
await test.step('7. 用户刷新页面', async () => {
await page.reload();
await expect(page.locator('table')).toContainText(`更新数据_${timestamp}`);
});
await test.step('8. 验证数据更新', async () => {
await expect(page.locator('table')).toContainText(`更新数据_${timestamp}`);
});
await test.step('9. 验证数据缓存', async () => {
await dictionaryManagementPage.refreshCache();
await expect(dictionaryManagementPage.successMessage).toBeVisible();
});
await test.step('10. 验证数据一致性', async () => {
await page.reload();
await expect(page.locator('table')).toContainText(`更新数据_${timestamp}`);
});
});
test('UAT-010: 多用户协作场景', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const userManagementPage = new UserManagementPage(page);
const timestamp = Date.now();
await test.step('1. 创建测试用户A', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
const userData = {
username: `user_a_${timestamp}`,
nickname: `用户A_${timestamp}`,
email: `user_a_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
testDataCleanup.trackUser(`user_a_${timestamp}`);
});
await test.step('2. 创建测试用户B', async () => {
await userManagementPage.clickCreateUser();
const userData = {
username: `user_b_${timestamp}`,
nickname: `用户B_${timestamp}`,
email: `user_b_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
testDataCleanup.trackUser(`user_b_${timestamp}`);
});
await test.step('3. 多用户同时登录', async () => {
await loginPage.logout();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('4. 用户A创建数据', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
const userData = {
username: `data_a_${timestamp}`,
nickname: `数据A_${timestamp}`,
email: `data_a_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('5. 用户B同时创建数据', async () => {
await userManagementPage.clickCreateUser();
const userData = {
username: `data_b_${timestamp}`,
nickname: `数据B_${timestamp}`,
email: `data_b_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible();
});
await test.step('6. 验证数据一致性', async () => {
await page.reload();
await expect(page.locator('table')).toContainText(`data_a_${timestamp}`);
await expect(page.locator('table')).toContainText(`data_b_${timestamp}`);
});
await test.step('7. 验证并发处理', async () => {
const userCount = await userManagementPage.getUserCount();
expect(userCount).toBeGreaterThanOrEqual(2);
});
await test.step('8. 查看操作日志', async () => {
await dashboardPage.navigateToOperationLog();
await expect(page.locator('table')).toContainText('创建');
});
await test.step('9. 验证日志完整性', async () => {
const logCount = await page.locator('table tbody tr').count();
expect(logCount).toBeGreaterThan(0);
});
await test.step('10. 清理测试数据', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.search(`user_a_${timestamp}`);
await page.waitForTimeout(1000);
const rows = await page.locator('table tbody tr').count();
if (rows > 0) {
await userManagementPage.deleteUser(1);
await userManagementPage.confirmDelete();
}
});
});
});
@@ -1,94 +0,0 @@
import { test, expect, Page } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { UserManagementPage } from './pages/UserManagementPage';
test.describe('关键业务流程E2E测试', () => {
let loginPage: LoginPage;
let userManagementPage: UserManagementPage;
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
loginPage = new LoginPage(page);
userManagementPage = new UserManagementPage(page);
});
test.afterEach(async ({ page }) => {
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
});
test('1. 用户登录流程', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await expect(page.locator('.dashboard')).toBeVisible();
const token = await page.evaluate(() => localStorage.getItem('token'));
expect(token).toBeTruthy();
});
test('2. 用户创建流程', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await userManagementPage.goto();
await userManagementPage.waitForTableReady();
const uuid = Math.random().toString(36).substring(2, 15);
const username = `user_${uuid}`;
await userManagementPage.clickCreateUser();
await userManagementPage.fillUserForm({
username: username,
password: 'Test@123',
email: `${username}@test.com`,
phone: '13800138000',
nickname: `测试用户${Date.now()}`
});
await userManagementPage.submitForm();
const success = await userManagementPage.waitForSuccessMessage();
expect(success).toBeTruthy();
});
test('3. 管理员权限验证', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await userManagementPage.goto();
await expect(userManagementPage.table).toBeVisible({ timeout: 5000 });
const userCount = await userManagementPage.getUserCount();
expect(userCount).toBeGreaterThan(0);
});
test('4. 未登录用户访问受保护页面', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForURL(/\/login/, { timeout: 10000 });
await expect(page.locator('input[placeholder="请输入用户名"]')).toBeVisible();
});
test('5. 登出流程', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
const avatar = page.locator('.el-avatar');
await avatar.click();
await page.waitForTimeout(1000);
const logoutButton = page.locator('.el-dropdown-menu').getByText('退出登录');
await logoutButton.click();
await page.waitForURL(/\/login/, { timeout: 10000 });
await expect(page.locator('input[placeholder="请输入用户名"]')).toBeVisible();
});
});
+29 -10
View File
@@ -36,17 +36,32 @@ class CustomReporter implements Reporter {
}
private calculateStats(result: FullResult): TestStats {
const suites = result.suites || [];
const allTests = suites.flatMap(suite =>
suite.specs.flatMap(spec => spec.tests)
);
const allTests = this.testResults;
if (allTests.length === 0) {
return {
total: 0,
passed: 0,
failed: 0,
skipped: 0,
flaky: 0,
passRate: 0,
failRate: 0,
skipRate: 0,
flakyRate: 0,
totalDuration: 0,
avgDuration: 0,
slowestTests: [],
failedTests: [],
};
}
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 totalDuration = allTests.reduce((sum, t) => sum + t.duration, 0);
const avgDuration = totalDuration / allTests.length;
const passRate = (passed.length / allTests.length) * 100;
@@ -67,8 +82,8 @@ class CustomReporter implements Reporter {
totalDuration,
avgDuration,
slowestTests: allTests
.filter(t => t.duration)
.sort((a, b) => (b.duration || 0) - (a.duration || 0))
.filter(t => t.duration > 0)
.sort((a, b) => b.duration - a.duration)
.slice(0, 10),
failedTests: failed,
};
@@ -98,9 +113,13 @@ class CustomReporter implements Reporter {
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(` ${index + 1}. ${test.title || '未命名测试'}`);
if (test.location?.file) {
console.log(` 位置: ${test.location.file}:${test.location.line || 0}`);
}
if (test.error?.message) {
console.log(` 错误: ${test.error.message}`);
}
});
console.log('');
}
@@ -1,185 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
test.describe('Dashboard操作日志显示验证', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
test.afterEach(async ({ page }) => {
await loginPage.logout();
});
test('Dashboard应显示操作日志统计卡片', async ({ page }) => {
await test.step('验证操作日志统计卡片存在', async () => {
const operationLogCard = page.locator('.stat-card.log-card');
await expect(operationLogCard).toBeVisible();
});
await test.step('验证操作日志统计标题', async () => {
const title = page.locator('.stat-card.log-card .el-statistic__head');
await expect(title).toContainText('操作日志');
});
await test.step('验证操作日志统计数值', async () => {
const value = page.locator('.stat-card.log-card .el-statistic__number');
await expect(value).toBeVisible();
const countText = await value.textContent();
expect(countText).not.toBeNull();
const count = parseInt(countText!);
expect(count).toBeGreaterThanOrEqual(0);
});
});
test('Dashboard应显示其他统计卡片', async ({ page }) => {
await test.step('验证用户总数卡片', async () => {
const userCard = page.locator('.stat-card.user-card');
await expect(userCard).toBeVisible();
const title = userCard.locator('.el-statistic__head');
await expect(title).toContainText('用户总数');
});
await test.step('验证角色总数卡片', async () => {
const roleCard = page.locator('.stat-card.role-card');
await expect(roleCard).toBeVisible();
const title = roleCard.locator('.el-statistic__head');
await expect(title).toContainText('角色总数');
});
await test.step('验证今日登录卡片', async () => {
const loginCard = page.locator('.stat-card.login-card');
await expect(loginCard).toBeVisible();
const title = loginCard.locator('.el-statistic__head');
await expect(title).toContainText('今日登录');
});
});
test('Dashboard统计卡片应显示图标', async ({ page }) => {
await test.step('验证操作日志图标', async () => {
const icon = page.locator('.stat-card.log-card .stat-icon');
await expect(icon).toBeVisible();
});
await test.step('验证用户图标', async () => {
const icon = page.locator('.stat-card.user-card .stat-icon');
await expect(icon).toBeVisible();
});
await test.step('验证角色图标', async () => {
const icon = page.locator('.stat-card.role-card .stat-icon');
await expect(icon).toBeVisible();
});
await test.step('验证登录图标', async () => {
const icon = page.locator('.stat-card.login-card .stat-icon');
await expect(icon).toBeVisible();
});
});
test('Dashboard统计卡片应有悬停效果', async ({ page }) => {
await test.step('验证操作日志卡片悬停效果', async () => {
const card = page.locator('.stat-card.log-card');
await card.hover();
await page.waitForTimeout(500);
await expect(card).toBeVisible();
});
});
test('Dashboard应显示最近登录记录', async ({ page }) => {
await test.step('验证最近登录卡片存在', async () => {
const recentLoginCard = page.locator('.recent-login-card');
await expect(recentLoginCard).toBeVisible();
});
await test.step('验证最近登录标题', async () => {
const title = page.locator('.recent-login-card .card-title');
await expect(title).toContainText('最近登录');
});
});
test('Dashboard应显示系统信息', async ({ page }) => {
await test.step('验证系统信息卡片存在', async () => {
const systemInfoCard = page.locator('.system-info-card');
await expect(systemInfoCard).toBeVisible();
});
await test.step('验证系统信息标题', async () => {
const title = page.locator('.system-info-card .card-title');
await expect(title).toContainText('系统信息');
});
await test.step('验证系统版本显示', async () => {
const versionItem = page.locator('.system-info-card').getByText('系统版本');
await expect(versionItem).toBeVisible();
});
await test.step('验证Java版本显示', async () => {
const javaItem = page.locator('.system-info-card').getByText('Java版本');
await expect(javaItem).toBeVisible();
});
await test.step('验证前端框架显示', async () => {
const frontendItem = page.locator('.system-info-card').getByText('前端框架');
await expect(frontendItem).toBeVisible();
});
await test.step('验证数据库显示', async () => {
const dbItem = page.locator('.system-info-card').getByText('数据库');
await expect(dbItem).toBeVisible();
});
});
test('Dashboard操作日志统计应正确反映实际数据', async ({ page }) => {
await test.step('获取Dashboard显示的操作日志数量', async () => {
const value = page.locator('.stat-card.log-card .el-statistic__number');
await expect(value).toBeVisible();
const countText = await value.textContent();
expect(countText).not.toBeNull();
const dashboardCount = parseInt(countText!);
expect(dashboardCount).toBeGreaterThanOrEqual(0);
});
});
test('Dashboard页面加载性能', async ({ page }) => {
await test.step('验证页面加载时间', async () => {
const startTime = Date.now();
await dashboardPage.goto();
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(10000);
});
await test.step('验证统计卡片加载', async () => {
const cards = page.locator('.stat-card');
await expect(cards.first()).toBeVisible({ timeout: 5000 });
});
});
test('Dashboard响应式布局验证', async ({ page }) => {
await test.step('验证桌面端布局', async () => {
await page.setViewportSize({ width: 1280, height: 720 });
const cards = page.locator('.stat-card');
expect(await cards.count()).toBe(4);
});
await test.step('验证平板端布局', async () => {
await page.setViewportSize({ width: 768, height: 1024 });
const cards = page.locator('.stat-card');
expect(await cards.count()).toBe(4);
});
await test.step('验证移动端布局', async () => {
await page.setViewportSize({ width: 375, height: 667 });
const cards = page.locator('.stat-card');
expect(await cards.count()).toBe(4);
});
});
});
@@ -1,72 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';
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);
// 监控所有网络请求
page.on('request', request => {
console.log(`>> REQUEST: ${request.method()} ${request.url()}`);
if (request.method() === 'POST' || request.method() === 'PUT') {
console.log(` POST DATA: ${request.postData()}`);
}
});
page.on('response', response => {
console.log(`<< RESPONSE: ${response.status()} ${response.url()}`);
if (response.status() >= 400) {
console.log(` ❌ ERROR RESPONSE: ${response.status()} ${response.url()}`);
}
});
// 清理localStorage
await page.goto('/');
await page.evaluate(() => localStorage.clear());
// 重新登录
await loginPage.goto();
await loginPage.login('e2e_test_user', 'admin123');
});
test('创建用户 - 带网络监控', async ({ page }) => {
console.log('\n========== 开始创建用户测试 ==========\n');
await dashboardPage.navigateToUserManagement();
console.log('✅ 导航到用户管理页面');
await userManagementPage.clickCreateUser();
console.log('✅ 点击创建用户按钮');
const timestamp = Date.now();
const userData = {
username: `testuser_${timestamp}`,
nickname: `测试用户${timestamp}`,
email: `test_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
console.log(`📝 填写用户数据: ${JSON.stringify(userData)}`);
await userManagementPage.fillUserForm(userData);
console.log('✅ 填写用户表单完成');
console.log('📤 准备提交表单...');
await userManagementPage.submitForm();
console.log('✅ 表单已提交');
// 等待一段时间,观察网络请求
await page.waitForTimeout(5000);
console.log('\n========== 测试结束 ==========\n');
});
});
@@ -1,79 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test.describe('登录诊断测试', () => {
test('诊断登录问题', async ({ page }) => {
const loginPage = new LoginPage(page);
console.log('=== 开始诊断登录问题 ===');
await loginPage.goto();
console.log('1. 登录页面加载成功');
await page.screenshot({ path: 'test-results/diagnostic/01-login-page.png', fullPage: true });
console.log('2. 截图已保存: 01-login-page.png');
const usernameVisible = await loginPage.usernameInput.isVisible();
const passwordVisible = await loginPage.passwordInput.isVisible();
const loginButtonVisible = await loginPage.loginButton.isVisible();
console.log('3. 页面元素检查:');
console.log(` - 用户名输入框: ${usernameVisible ? '可见' : '不可见'}`);
console.log(` - 密码输入框: ${passwordVisible ? '可见' : '不可见'}`);
console.log(` - 登录按钮: ${loginButtonVisible ? '可见' : '不可见'}`);
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
console.log('4. 已填写用户名和密码');
await page.screenshot({ path: 'test-results/diagnostic/02-filled-form.png', fullPage: true });
console.log('5. 截图已保存: 02-filled-form.png');
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/auth/login') && response.request().method() === 'POST'
);
await loginPage.loginButton.click();
console.log('6. 已点击登录按钮');
try {
const response = await responsePromise;
console.log('7. 收到API响应:');
console.log(` - 状态码: ${response.status()}`);
console.log(` - URL: ${response.url()}`);
const responseBody = await response.text();
console.log(` - 响应体: ${responseBody.substring(0, 500)}`);
} catch (error) {
console.log('7. 未收到API响应或超时:', error);
}
await page.waitForTimeout(3000);
const currentUrl = page.url();
console.log(`8. 当前URL: ${currentUrl}`);
await page.screenshot({ path: 'test-results/diagnostic/03-after-login.png', fullPage: true });
console.log('9. 截图已保存: 03-after-login.png');
const errorMessage = await loginPage.getErrorMessage();
if (errorMessage) {
console.log(`10. 错误消息: ${errorMessage}`);
} else {
console.log('10. 没有错误消息');
}
const pageContent = await page.content();
console.log('11. 页面内容长度:', pageContent.length);
if (currentUrl.includes('dashboard')) {
console.log('✅ 登录成功!已跳转到仪表板');
} else if (currentUrl.includes('login')) {
console.log('❌ 登录失败!仍在登录页面');
} else {
console.log(`⚠️ 意外的URL: ${currentUrl}`);
}
console.log('=== 诊断完成 ===');
});
});
@@ -1,481 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DictionaryManagementPage } from './pages/DictionaryManagementPage';
test.describe('字典管理 E2E 测试', () => {
let loginPage: LoginPage;
let dictManagementPage: DictionaryManagementPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dictManagementPage = new DictionaryManagementPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
test('DICT-001: 访问字典管理页面', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
await expect(page).toHaveURL(/.*dict/);
});
await test.step('验证页面元素可见', async () => {
await expect(dictManagementPage.table).toBeVisible();
await expect(dictManagementPage.createDictTypeButton).toBeVisible();
await expect(dictManagementPage.searchInput).toBeVisible();
});
});
test('DICT-002: 创建字典类型', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('点击新增字典类型按钮', async () => {
await dictManagementPage.clickCreateDictType();
});
await test.step('填写字典类型信息', async () => {
const timestamp = Date.now();
const dictTypeData = {
dictName: `测试字典类型_${timestamp}`,
dictType: `test_dict_type_${timestamp}`,
status: '1',
remark: '这是一个测试字典类型'
};
await dictManagementPage.fillDictTypeForm(dictTypeData);
});
await test.step('提交表单', async () => {
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
await test.step('验证字典类型创建成功', async () => {
await dictManagementPage.reload();
const dictTypeCount = await dictManagementPage.getDictTypeCount();
expect(dictTypeCount).toBeGreaterThan(0);
});
});
test('DICT-003: 编辑字典类型', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('创建测试字典类型', async () => {
await dictManagementPage.clickCreateDictType();
const timestamp = Date.now();
const dictTypeData = {
dictName: `待编辑字典_${timestamp}`,
dictType: `edit_dict_${timestamp}`,
status: '1'
};
await dictManagementPage.fillDictTypeForm(dictTypeData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
await page.waitForTimeout(1000);
});
await test.step('编辑字典类型', async () => {
const timestamp = Date.now();
await dictManagementPage.editDictType(`待编辑字典_${timestamp}`);
await page.waitForTimeout(500);
const updateData = {
dictName: `已编辑字典_${timestamp}`,
remark: '这是更新后的备注'
};
await dictManagementPage.fillDictTypeForm(updateData);
});
await test.step('提交修改', async () => {
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('DICT-004: 删除字典类型', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('创建测试字典类型', async () => {
await dictManagementPage.clickCreateDictType();
const timestamp = Date.now();
const dictTypeData = {
dictName: `待删除字典_${timestamp}`,
dictType: `delete_dict_${timestamp}`,
status: '1'
};
await dictManagementPage.fillDictTypeForm(dictTypeData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
await page.waitForTimeout(1000);
});
await test.step('删除字典类型', async () => {
const timestamp = Date.now();
await dictManagementPage.deleteDictType(`待删除字典_${timestamp}`);
await dictManagementPage.confirmDelete();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
await test.step('验证字典类型已删除', async () => {
await dictManagementPage.reload();
const timestamp = Date.now();
const dictDeleted = await dictManagementPage.containsText(`待删除字典_${timestamp}`);
expect(dictDeleted).toBe(false);
});
});
test('DICT-005: 创建字典数据', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('点击新增字典数据按钮', async () => {
await dictManagementPage.clickCreateDictData();
});
await test.step('填写字典数据信息', async () => {
const timestamp = Date.now();
const dictData = {
dictLabel: `测试字典标签_${timestamp}`,
dictValue: `test_value_${timestamp}`,
dictType: 'sys_normal_disable',
cssClass: 'el-tag-success',
listClass: 'default',
isDefault: 'Y',
status: '1',
sort: 1
};
await dictManagementPage.fillDictDataForm(dictData);
});
await test.step('提交表单', async () => {
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
await test.step('验证字典数据创建成功', async () => {
await dictManagementPage.reload();
const dictDataCount = await dictManagementPage.getDictDataCount();
expect(dictDataCount).toBeGreaterThan(0);
});
});
test('DICT-006: 编辑字典数据', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('创建测试字典数据', async () => {
await dictManagementPage.clickCreateDictData();
const timestamp = Date.now();
const dictData = {
dictLabel: `待编辑标签_${timestamp}`,
dictValue: `edit_value_${timestamp}`,
dictType: 'sys_normal_disable',
status: '1',
sort: 1
};
await dictManagementPage.fillDictDataForm(dictData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
await page.waitForTimeout(1000);
});
await test.step('编辑字典数据', async () => {
const timestamp = Date.now();
await dictManagementPage.editDictData(`待编辑标签_${timestamp}`);
await page.waitForTimeout(500);
const updateData = {
dictLabel: `已编辑标签_${timestamp}`,
cssClass: 'el-tag-warning'
};
await dictManagementPage.fillDictDataForm(updateData);
});
await test.step('提交修改', async () => {
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('DICT-007: 删除字典数据', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('创建测试字典数据', async () => {
await dictManagementPage.clickCreateDictData();
const timestamp = Date.now();
const dictData = {
dictLabel: `待删除标签_${timestamp}`,
dictValue: `delete_value_${timestamp}`,
dictType: 'sys_normal_disable',
status: '1',
sort: 1
};
await dictManagementPage.fillDictDataForm(dictData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
await page.waitForTimeout(1000);
});
await test.step('删除字典数据', async () => {
const timestamp = Date.now();
await dictManagementPage.deleteDictData(`待删除标签_${timestamp}`);
await dictManagementPage.confirmDelete();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
await test.step('验证字典数据已删除', async () => {
await dictManagementPage.reload();
const timestamp = Date.now();
const dictDataDeleted = await dictManagementPage.containsText(`待删除标签_${timestamp}`);
expect(dictDataDeleted).toBe(false);
});
});
test('DICT-008: 搜索字典', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('搜索字典类型', async () => {
await dictManagementPage.search('系统');
await page.waitForTimeout(1000);
});
await test.step('验证搜索结果', async () => {
const searchResult = await dictManagementPage.containsText('系统');
expect(searchResult).toBe(true);
});
await test.step('清除搜索', async () => {
await dictManagementPage.search('');
await page.waitForTimeout(1000);
const dictTypeCount = await dictManagementPage.getDictTypeCount();
expect(dictTypeCount).toBeGreaterThan(0);
});
});
test('DICT-009: 字典状态管理', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('创建启用状态的字典类型', async () => {
await dictManagementPage.clickCreateDictType();
const timestamp = Date.now();
const dictTypeData = {
dictName: `启用字典_${timestamp}`,
dictType: `enabled_dict_${timestamp}`,
status: '1',
remark: '这是启用的字典'
};
await dictManagementPage.fillDictTypeForm(dictTypeData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
await test.step('创建禁用状态的字典类型', async () => {
await dictManagementPage.clickCreateDictType();
const timestamp = Date.now();
const dictTypeData = {
dictName: `禁用字典_${timestamp}`,
dictType: `disabled_dict_${timestamp}`,
status: '0',
remark: '这是禁用的字典'
};
await dictManagementPage.fillDictTypeForm(dictTypeData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('DICT-010: 字典排序功能', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('创建多个字典数据测试排序', async () => {
for (let i = 1; i <= 3; i++) {
await dictManagementPage.clickCreateDictData();
const timestamp = Date.now();
const dictData = {
dictLabel: `排序标签_${i}_${timestamp}`,
dictValue: `sort_value_${i}_${timestamp}`,
dictType: 'sys_normal_disable',
status: '1',
sort: i
};
await dictManagementPage.fillDictDataForm(dictData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
await page.waitForTimeout(500);
}
});
await test.step('验证字典数据按排序号显示', async () => {
await dictManagementPage.reload();
await page.waitForTimeout(1000);
const dictDataCount = await dictManagementPage.getDictDataCount();
expect(dictDataCount).toBeGreaterThan(0);
});
});
test('DICT-011: 字典默认值设置', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('创建默认字典数据', async () => {
await dictManagementPage.clickCreateDictData();
const timestamp = Date.now();
const dictData = {
dictLabel: `默认标签_${timestamp}`,
dictValue: `default_value_${timestamp}`,
dictType: 'sys_normal_disable',
isDefault: 'Y',
status: '1',
sort: 1
};
await dictManagementPage.fillDictDataForm(dictData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
await test.step('创建非默认字典数据', async () => {
await dictManagementPage.clickCreateDictData();
const timestamp = Date.now();
const dictData = {
dictLabel: `非默认标签_${timestamp}`,
dictValue: `non_default_value_${timestamp}`,
dictType: 'sys_normal_disable',
isDefault: 'N',
status: '1',
sort: 2
};
await dictManagementPage.fillDictDataForm(dictData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('DICT-012: 字典CSS样式配置', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('创建带CSS样式的字典数据', async () => {
await dictManagementPage.clickCreateDictData();
const timestamp = Date.now();
const dictData = {
dictLabel: `样式标签_${timestamp}`,
dictValue: `style_value_${timestamp}`,
dictType: 'sys_normal_disable',
cssClass: 'el-tag-success',
listClass: 'default',
status: '1',
sort: 1
};
await dictManagementPage.fillDictDataForm(dictData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('DICT-013: 字典数据验证', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('验证字典类型数据完整性', async () => {
const dictTypeCount = await dictManagementPage.getDictTypeCount();
expect(dictTypeCount).toBeGreaterThan(0);
});
await test.step('验证字典数据完整性', async () => {
const dictDataCount = await dictManagementPage.getDictDataCount();
expect(dictDataCount).toBeGreaterThan(0);
});
await test.step('验证表格包含必要列', async () => {
await expect(dictManagementPage.table).toContainText('字典名称');
await expect(dictManagementPage.table).toContainText('字典类型');
await expect(dictManagementPage.table).toContainText('状态');
});
});
test('DICT-014: 字典响应式布局', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('验证桌面端布局', async () => {
await page.setViewportSize({ width: 1280, height: 720 });
await expect(dictManagementPage.table).toBeVisible();
await expect(dictManagementPage.createDictTypeButton).toBeVisible();
});
await test.step('验证平板端布局', async () => {
await page.setViewportSize({ width: 768, height: 1024 });
await expect(dictManagementPage.table).toBeVisible();
await expect(dictManagementPage.createDictTypeButton).toBeVisible();
});
await test.step('验证移动端布局', async () => {
await page.setViewportSize({ width: 375, height: 667 });
await expect(dictManagementPage.table).toBeVisible();
});
});
test('DICT-015: 字典类型与数据关联', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictManagementPage.goto();
});
await test.step('创建字典类型', async () => {
await dictManagementPage.clickCreateDictType();
const timestamp = Date.now();
const dictTypeData = {
dictName: `关联测试字典_${timestamp}`,
dictType: `relation_dict_${timestamp}`,
status: '1',
remark: '用于测试类型与数据关联'
};
await dictManagementPage.fillDictTypeForm(dictTypeData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
await page.waitForTimeout(1000);
});
await test.step('为该类型创建多个字典数据', async () => {
for (let i = 1; i <= 3; i++) {
await dictManagementPage.clickCreateDictData();
const timestamp = Date.now();
const dictData = {
dictLabel: `关联数据_${i}_${timestamp}`,
dictValue: `relation_value_${i}_${timestamp}`,
dictType: `relation_dict_${timestamp}`,
status: '1',
sort: i
};
await dictManagementPage.fillDictDataForm(dictData);
await dictManagementPage.submitForm();
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
await page.waitForTimeout(500);
}
});
await test.step('验证字典数据关联成功', async () => {
await dictManagementPage.reload();
const dictDataCount = await dictManagementPage.getDictDataCount();
expect(dictDataCount).toBeGreaterThanOrEqual(3);
});
});
});
-534
View File
@@ -1,534 +0,0 @@
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('创建成功');
});
});
});
});
@@ -1,238 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { ExceptionLogPage } from './pages/ExceptionLogPage';
test.describe('异常日志 E2E 测试', () => {
let loginPage: LoginPage;
let exceptionLogPage: ExceptionLogPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
exceptionLogPage = new ExceptionLogPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
test.afterEach(async ({ page }) => {
await loginPage.logout();
});
test('EXCEPTION-001: 访问异常日志页面', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
await expect(page).toHaveURL(/.*exceptionlog/);
});
await test.step('验证页面元素可见', async () => {
await expect(exceptionLogPage.table).toBeVisible();
await expect(exceptionLogPage.searchInput).toBeVisible();
await expect(exceptionLogPage.exportButton).toBeVisible();
});
});
test('EXCEPTION-002: 搜索异常日志', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('搜索异常日志', async () => {
const keyword = 'admin';
await exceptionLogPage.search(keyword);
await exceptionLogPage.verifyTableContains(keyword);
});
await test.step('清除搜索', async () => {
await exceptionLogPage.clearSearch();
const rowCount = await exceptionLogPage.getLogCount();
expect(rowCount).toBeGreaterThanOrEqual(0);
});
});
test('EXCEPTION-003: 异常日志分页功能', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('验证表格数据加载', async () => {
const rowCount = await exceptionLogPage.getLogCount();
expect(rowCount).toBeGreaterThanOrEqual(0);
});
});
test('EXCEPTION-004: 异常日志响应式布局', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('验证桌面端布局', async () => {
await page.setViewportSize({ width: 1280, height: 720 });
await expect(exceptionLogPage.table).toBeVisible();
await expect(exceptionLogPage.exportButton).toBeVisible();
});
await test.step('验证平板端布局', async () => {
await page.setViewportSize({ width: 768, height: 1024 });
await expect(exceptionLogPage.table).toBeVisible();
await expect(exceptionLogPage.exportButton).toBeVisible();
});
await test.step('验证移动端布局', async () => {
await page.setViewportSize({ width: 375, height: 667 });
await expect(exceptionLogPage.table).toBeVisible();
});
});
test('EXCEPTION-005: 异常日志数据验证', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('验证日志数据完整性', async () => {
const rowCount = await exceptionLogPage.getLogCount();
expect(rowCount).toBeGreaterThanOrEqual(0);
});
await test.step('验证日志字段显示', async () => {
await expect(exceptionLogPage.table).toBeVisible();
});
});
test('EXCEPTION-006: 异常日志搜索功能', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('按用户名搜索', async () => {
const operator = 'admin';
await exceptionLogPage.search(operator);
await exceptionLogPage.verifyTableContains(operator);
});
await test.step('按异常信息搜索', async () => {
const exceptionInfo = 'Exception';
await exceptionLogPage.search(exceptionInfo);
});
await test.step('清除搜索结果', async () => {
await exceptionLogPage.clearSearch();
const rowCount = await exceptionLogPage.getLogCount();
expect(rowCount).toBeGreaterThanOrEqual(0);
});
});
test('EXCEPTION-007: 异常日志导出功能', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('导出异常日志', async () => {
const downloadPromise = page.waitForEvent('download');
await exceptionLogPage.exportData();
const download = await downloadPromise;
expect(download).toBeDefined();
});
});
test('EXCEPTION-008: 异常日志时间范围验证', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('验证日志时间戳显示', async () => {
const rowCount = await exceptionLogPage.getLogCount();
if (rowCount > 0) {
await expect(exceptionLogPage.table).toBeVisible();
}
});
});
test('EXCEPTION-009: 异常日志权限验证', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('验证导出按钮可见性', async () => {
await expect(exceptionLogPage.exportButton).toBeVisible();
});
await test.step('验证搜索功能可用', async () => {
await expect(exceptionLogPage.searchInput).toBeVisible();
await expect(exceptionLogPage.searchButton).toBeVisible();
});
});
test('EXCEPTION-010: 异常日志详情查看', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('验证日志详情显示', async () => {
const rowCount = await exceptionLogPage.getLogCount();
if (rowCount > 0) {
await expect(exceptionLogPage.table).toBeVisible();
}
});
});
test('EXCEPTION-011: 异常日志刷新功能', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('刷新异常日志', async () => {
await exceptionLogPage.refresh();
await expect(exceptionLogPage.table).toBeVisible();
});
});
test('EXCEPTION-012: 异常日志排序功能', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('验证表格排序功能', async () => {
const rowCount = await exceptionLogPage.getLogCount();
if (rowCount > 0) {
await expect(exceptionLogPage.table).toBeVisible();
}
});
});
test('EXCEPTION-013: 异常日志空状态显示', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('搜索不存在的异常', async () => {
await exceptionLogPage.search('nonexistent_exception_123456');
await page.waitForTimeout(1000);
});
await test.step('验证空状态显示', async () => {
const rowCount = await exceptionLogPage.getLogCount();
expect(rowCount).toBe(0);
});
});
test('EXCEPTION-014: 异常日志批量操作', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('验证批量操作按钮可见性', async () => {
await expect(exceptionLogPage.table).toBeVisible();
});
});
test('EXCEPTION-015: 异常日志详细信息验证', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('验证异常日志包含必要信息', async () => {
await expect(exceptionLogPage.table).toBeVisible();
});
});
});
@@ -1,205 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { FileManagementPage } from './pages/FileManagementPage';
test.describe('文件管理 E2E 测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let fileManagementPage: FileManagementPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
fileManagementPage = new FileManagementPage(page);
});
test('FILE-001: 管理员查看文件列表', async ({ page }) => {
await test.step('管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('导航到文件管理页面', async () => {
await page.goto('/files');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000);
});
await test.step('验证文件列表页面加载', async () => {
await expect(fileManagementPage.table).toBeVisible();
const rowCount = await fileManagementPage.getTableRowCount();
expect(rowCount).toBeGreaterThanOrEqual(0);
});
await test.step('验证文件表格包含必要列', async () => {
await expect(fileManagementPage.table).toContainText('文件名');
await expect(fileManagementPage.table).toContainText('文件大小');
await expect(fileManagementPage.table).toContainText('上传时间');
await expect(fileManagementPage.table).toContainText('上传人');
});
});
test('FILE-002: 上传文件', async ({ page }) => {
await test.step('管理员登录并导航到文件管理', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await fileManagementPage.goto();
});
await test.step('上传测试文件', async () => {
const testFilePath = './e2e/fixtures/test-file.txt';
const uploadButton = page.locator('.el-upload');
await uploadButton.first().click();
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFilePath);
await page.waitForTimeout(3000);
await expect(fileManagementPage.table).toBeVisible();
});
});
test('FILE-003: 搜索文件', async ({ page }) => {
await test.step('管理员登录并导航到文件管理', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await fileManagementPage.goto();
});
await test.step('搜索特定文件', async () => {
await fileManagementPage.searchFile('test');
await page.waitForTimeout(1000);
});
await test.step('清除搜索条件', async () => {
await fileManagementPage.clearSearch();
const rowCount = await fileManagementPage.getTableRowCount();
expect(rowCount).toBeGreaterThanOrEqual(0);
});
});
test('FILE-004: 下载文件', async ({ page, context }) => {
await test.step('管理员登录并导航到文件管理', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await fileManagementPage.goto();
});
await test.step('下载文件', async () => {
const rows = await fileManagementPage.table.locator('.el-table__row').count();
if (rows > 0) {
const pagePromise = context.waitForEvent('page');
await fileManagementPage.downloadFile('test');
const newPage = await pagePromise;
expect(newPage).toBeDefined();
await newPage.close();
}
});
});
test('FILE-005: 删除文件', async ({ page }) => {
await test.step('管理员登录并导航到文件管理', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await fileManagementPage.goto();
});
await test.step('删除文件', async () => {
const rows = await fileManagementPage.table.locator('.el-table__row').count();
if (rows > 0) {
const firstRow = fileManagementPage.table.locator('.el-table__row').first();
const fileName = await firstRow.locator('td').nth(1).textContent();
if (fileName) {
await fileManagementPage.deleteFile(fileName);
await page.waitForTimeout(1000);
await expect(fileManagementPage.table).toBeVisible();
}
}
});
});
test('FILE-006: 验证文件权限控制', async ({ page }) => {
await test.step('普通用户登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('尝试访问文件管理页面', async () => {
await page.goto('/files');
await page.waitForTimeout(2000);
const currentURL = page.url();
if (currentURL.includes('/files')) {
const rows = await fileManagementPage.table.locator('.el-table__row').count();
if (rows > 0) {
await expect(fileManagementPage.table).toBeVisible();
}
} else {
await expect(page).toHaveURL(/.*dashboard/);
}
});
});
test('FILE-007: 验证文件列表排序', async ({ page }) => {
await test.step('管理员登录并导航到文件管理', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await fileManagementPage.goto();
});
await test.step('验证文件按上传时间排序', async () => {
const rows = await fileManagementPage.table.locator('.el-table__row').count();
if (rows > 0) {
const firstRow = fileManagementPage.table.locator('.el-table__row').first();
await expect(firstRow).toBeVisible();
}
});
});
test('FILE-008: 验证文件大小显示', async ({ page }) => {
await test.step('管理员登录并导航到文件管理', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await fileManagementPage.goto();
});
await test.step('验证文件大小列显示', async () => {
await expect(fileManagementPage.table).toContainText('文件大小');
});
});
test('FILE-009: 验证文件上传人信息', async ({ page }) => {
await test.step('管理员登录并导航到文件管理', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await fileManagementPage.goto();
});
await test.step('验证上传人列显示', async () => {
await expect(fileManagementPage.table).toContainText('上传人');
});
});
test('FILE-010: 验证文件操作按钮可见性', async ({ page }) => {
await test.step('管理员登录并导航到文件管理', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await fileManagementPage.goto();
});
await test.step('验证表格可见', async () => {
await expect(fileManagementPage.table).toBeVisible();
});
await test.step('验证搜索功能可用', async () => {
const searchInput = page.locator('.search-bar input');
await expect(searchInput).toBeVisible();
});
});
});
-63
View File
@@ -1,63 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('登录表单验证测试', () => {
test('验证fill方法是否触发Vue响应式更新', async ({ page }) => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
// 使用fill方法填充
await page.locator('input[placeholder="请输入用户名"]').fill('admin');
await page.locator('input[placeholder="请输入密码"]').fill('admin123');
// 检查input元素的值
const usernameValue = await page.locator('input[placeholder="请输入用户名"]').inputValue();
const passwordValue = await page.locator('input[placeholder="请输入密码"]').inputValue();
console.log('Username input value:', usernameValue);
console.log('Password input value:', passwordValue);
// 检查Vue组件的状态
const formState = await page.evaluate(() => {
const app = document.querySelector('#app');
return app?.__vue_app__?.config?.globalProperties?.$data;
});
console.log('Vue formState:', formState);
// 尝试获取localStorage中的值(登录前应该为空)
const tokenBefore = await page.evaluate(() => localStorage.getItem('token'));
console.log('Token before login:', tokenBefore);
// 点击登录按钮
await page.locator('button:has-text("登录")').click();
// 等待API响应
const response = await page.waitForResponse(response =>
response.url().includes('/api/auth/login') && response.request().method() === 'POST',
{ timeout: 10000 }
).catch(e => {
console.log('No API response received:', e);
return null;
});
if (response) {
console.log('API response status:', response.status());
const responseBody = await response.text();
console.log('API response body:', responseBody.substring(0, 200));
}
// 等待一段时间
await page.waitForTimeout(3000);
// 检查localStorage中的token
const tokenAfter = await page.evaluate(() => localStorage.getItem('token'));
console.log('Token after login:', tokenAfter ? 'exists' : 'not found');
// 检查当前URL
const currentUrl = page.url();
console.log('Current URL:', currentUrl);
// 截图
await page.screenshot({ path: 'test-results/form-test.png', fullPage: true });
});
});
+388 -98
View File
@@ -8,8 +8,21 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let backendProcess: ChildProcess | null = null;
let gatewayProcess: ChildProcess | null = null;
let healthCheckInterval: NodeJS.Timeout | null = null;
function renderProgressBar(label: string, current: number, total: number, width: number = 30): void {
const ratio = Math.min(current / total, 1);
const filled = Math.round(ratio * width);
const empty = width - filled;
const bar = '█'.repeat(filled) + '░'.repeat(empty);
const percent = (ratio * 100).toFixed(0);
process.stdout.write(`\r ${label} [${bar}] ${percent}% (${current}/${total}s)`);
if (ratio >= 1) {
process.stdout.write('\n');
}
}
async function checkBackendHealth(): Promise<boolean> {
try {
const response = await fetch('http://localhost:8084/actuator/health', {
@@ -25,16 +38,51 @@ async function checkBackendHealth(): Promise<boolean> {
}
}
async function checkGatewayHealth(): Promise<boolean> {
try {
const response = await fetch('http://localhost:8080/actuator/health', {
signal: AbortSignal.timeout(5000)
} as any);
if (response.ok) {
const data = await response.json();
return data.status === 'UP';
}
return false;
} catch (error) {
return false;
}
}
async function checkFrontendHealth(): Promise<boolean> {
try {
const response = await fetch('http://localhost:3002', {
signal: AbortSignal.timeout(5000)
} as any);
return response.ok;
} catch (error) {
return false;
}
}
function startHealthMonitoring() {
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
}
healthCheckInterval = setInterval(async () => {
const isHealthy = await checkBackendHealth();
if (!isHealthy) {
const backendHealthy = await checkBackendHealth();
const gatewayHealthy = await checkGatewayHealth();
const frontendHealthy = await checkFrontendHealth();
if (!backendHealthy) {
console.error('⚠️ 后端服务健康检查失败!');
}
if (!gatewayHealthy) {
console.error('⚠️ 网关服务健康检查失败!');
}
if (!frontendHealthy) {
console.error('⚠️ 前端服务健康检查失败!');
}
}, 30000);
}
@@ -47,147 +95,365 @@ function stopHealthMonitoring() {
async function globalSetup(config: FullConfig) {
console.log('🚀 开始全局测试环境设置...');
process.env.NODE_ENV = 'test';
process.env.PLAYWRIGHT_HEADLESS = 'false';
const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app');
const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar');
let backendCommand: string;
let backendArgs: string[];
if (existsSync(jarFile)) {
console.log('📦 使用JAR文件启动后端服务...');
console.log(` JAR文件: ${jarFile}`);
backendCommand = 'java';
backendArgs = [
'-jar',
jarFile,
'--spring.profiles.active=test',
'-Xms256m',
'-Xmx512m'
];
const backendAlreadyRunning = await checkBackendHealth();
if (backendAlreadyRunning) {
console.log('✅ 后端服务已在运行,跳过启动');
} else {
console.log('📦 使用Maven启动后端服务...');
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
backendCommand = 'mvn';
backendArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=test'];
}
console.log(` 目录: ${backendDir}`);
console.log(` 命令: ${backendCommand} ${backendArgs.join(' ')}`);
backendProcess = spawn(backendCommand, backendArgs, {
cwd: backendDir,
stdio: 'pipe',
shell: true,
detached: false,
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'test' }
});
if (backendProcess.stdout) {
backendProcess.stdout.on('data', (data) => {
const output = data.toString();
if (output.includes('Started ManageApplication') || output.includes('Tomcat started on port')) {
console.log('✅ 后端服务启动成功');
}
});
}
if (backendProcess.stderr) {
backendProcess.stderr.on('data', (data) => {
const output = data.toString();
if (output.includes('ERROR') || output.includes('Exception')) {
console.error('❌ 后端服务启动错误:', output);
}
});
}
backendProcess.on('error', (error) => {
console.error('❌ 后端服务启动失败:', error);
});
backendProcess.on('exit', (code, signal) => {
if (code !== 0 && code !== null) {
console.error(`❌ 后端服务异常退出,退出码: ${code}, 信号: ${signal}`);
const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app');
const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar');
let backendCommand: string;
let backendArgs: string[];
if (existsSync(jarFile)) {
console.log('📦 使用JAR文件启动后端服务...');
console.log(` JAR文件: ${jarFile}`);
backendCommand = 'java';
backendArgs = [
'-jar',
jarFile,
'--spring.profiles.active=test',
'-Xms256m',
'-Xmx512m'
];
} else {
console.log('📦 使用Maven启动后端服务...');
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
backendCommand = 'mvn';
backendArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=test'];
}
});
console.log('⏳ 等待后端服务就绪...');
await waitForBackendReady();
console.log(` 目录: ${backendDir}`);
console.log(` 命令: ${backendCommand} ${backendArgs.join(' ')}`);
backendProcess = spawn(backendCommand, backendArgs, {
cwd: backendDir,
stdio: 'pipe',
shell: true,
detached: false,
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'test' }
});
if (backendProcess.stdout) {
backendProcess.stdout.on('data', (data) => {
const output = data.toString();
if (output.includes('Started ManageApplication') || output.includes('Tomcat started on port')) {
console.log('✅ 后端服务启动成功');
}
});
}
if (backendProcess.stderr) {
backendProcess.stderr.on('data', (data) => {
const output = data.toString();
if (output.includes('ERROR') || output.includes('Exception')) {
console.error('❌ 后端服务启动错误:', output);
}
});
}
backendProcess.on('error', (error) => {
console.error('❌ 后端服务启动失败:', error);
});
backendProcess.on('exit', (code, signal) => {
if (code !== 0 && code !== null) {
console.error(`❌ 后端服务异常退出,退出码: ${code}, 信号: ${signal}`);
}
});
console.log('⏳ 等待后端服务就绪...');
await waitForBackendReady();
}
const gatewayAlreadyRunning = await checkGatewayHealth();
if (gatewayAlreadyRunning) {
console.log('✅ 网关服务已在运行,跳过启动');
} else {
const gatewayDir = path.resolve(__dirname, '../../novalon-manage-api/manage-gateway');
const gatewayJarFile = path.join(gatewayDir, 'target/manage-gateway-1.0.0.jar');
let gatewayCommand: string;
let gatewayArgs: string[];
if (existsSync(gatewayJarFile)) {
console.log('🚪 使用JAR文件启动网关服务...');
console.log(` JAR文件: ${gatewayJarFile}`);
gatewayCommand = 'java';
gatewayArgs = [
'-jar',
gatewayJarFile,
'--spring.profiles.active=dev',
'-Xms128m',
'-Xmx256m'
];
} else {
console.log('🚪 使用Maven启动网关服务...');
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
gatewayCommand = 'mvn';
gatewayArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=dev'];
}
console.log(` 目录: ${gatewayDir}`);
console.log(` 命令: ${gatewayCommand} ${gatewayArgs.join(' ')}`);
gatewayProcess = spawn(gatewayCommand, gatewayArgs, {
cwd: gatewayDir,
stdio: 'pipe',
shell: true,
detached: false,
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'dev' }
});
if (gatewayProcess.stdout) {
gatewayProcess.stdout.on('data', (data) => {
const output = data.toString();
if (output.includes('Started GatewayApplication') || output.includes('Netty started on port')) {
console.log('✅ 网关服务启动成功');
}
});
}
if (gatewayProcess.stderr) {
gatewayProcess.stderr.on('data', (data) => {
const output = data.toString();
if (output.includes('ERROR') || output.includes('Exception')) {
console.error('❌ 网关服务启动错误:', output);
}
});
}
gatewayProcess.on('error', (error) => {
console.error('❌ 网关服务启动失败:', error);
});
gatewayProcess.on('exit', (code, signal) => {
if (code !== 0 && code !== null) {
console.error(`❌ 网关服务异常退出,退出码: ${code}, 信号: ${signal}`);
}
});
console.log('⏳ 等待网关服务就绪...');
await waitForGatewayReady();
}
console.log('🔍 验证所有服务连通性...');
await verifyAllServices();
console.log('🧹 清理测试数据...');
await cleanupTestData();
startHealthMonitoring();
console.log('✅ 全局测试环境设置完成');
}
async function verifyAllServices(): Promise<void> {
console.log(' 验证后端服务...');
const backendOk = await checkBackendHealth();
if (!backendOk) {
throw new Error('❌ 后端服务验证失败');
}
console.log(' ✅ 后端服务正常');
console.log(' 验证网关服务...');
const gatewayOk = await checkGatewayHealth();
if (!gatewayOk) {
throw new Error('❌ 网关服务验证失败');
}
console.log(' ✅ 网关服务正常');
console.log(' 验证网关到后端的连通性...');
try {
const response = await fetch('http://localhost:8080/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
signal: AbortSignal.timeout(10000) as any
});
if (!response.ok) {
throw new Error(`网关到后端连通性验证失败,状态码: ${response.status}`);
}
const data = await response.json();
if (!data.token) {
throw new Error('网关到后端连通性验证失败,未返回token');
}
console.log(' ✅ 网关到后端连通性正常');
} catch (error) {
throw new Error(`❌ 网关到后端连通性验证失败: ${error}`);
}
console.log('✅ 所有服务验证通过');
}
async function waitForBackendReady(): Promise<void> {
const maxRetries = 60;
const maxRetries = 90;
const retryInterval = 1000;
for (let i = 0; i < maxRetries; i++) {
renderProgressBar('⏳ 后端服务启动中', i, maxRetries);
try {
const response = await fetch('http://localhost:8084/actuator/health');
const response = await fetch('http://localhost:8084/actuator/health', {
signal: AbortSignal.timeout(5000) as any
});
if (response.ok) {
const data = await response.json();
if (data.status === 'UP') {
process.stdout.write('\r' + ' '.repeat(80) + '\r');
console.log(`✅ 后端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
return;
try {
const loginTest = await fetch('http://localhost:8084/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
signal: AbortSignal.timeout(10000) as any
});
if (loginTest.ok) {
console.log('✅ 后端服务连通性验证通过(登录API可用)');
return;
} else {
console.log(`⚠️ 后端服务连通性验证失败,状态码: ${loginTest.status}`);
}
} catch (error) {
console.log('⚠️ 后端服务连通性验证失败,继续等待...');
}
}
}
} catch (error) {
// 服务还未就绪,继续等待
}
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryInterval));
}
}
throw new Error('❌ 后端服务启动超时');
}
async function waitForGatewayReady(): Promise<void> {
const maxRetries = 90;
const retryInterval = 1000;
for (let i = 0; i < maxRetries; i++) {
renderProgressBar('⏳ 网关服务启动中', i, maxRetries);
try {
const response = await fetch('http://localhost:8080/actuator/health', {
signal: AbortSignal.timeout(5000) as any
});
if (response.ok) {
const data = await response.json();
if (data.status === 'UP') {
process.stdout.write('\r' + ' '.repeat(80) + '\r');
console.log(`✅ 网关服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
try {
const loginTest = await fetch('http://localhost:8080/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
signal: AbortSignal.timeout(10000) as any
});
if (loginTest.ok) {
console.log('✅ 网关服务连通性验证通过(登录API可用)');
return;
} else {
console.log(`⚠️ 网关服务连通性验证失败,状态码: ${loginTest.status}`);
}
} catch (error) {
console.log('⚠️ 网关服务连通性验证失败,继续等待...');
}
}
}
} catch (error) {
// 服务还未就绪,继续等待
}
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryInterval));
}
}
throw new Error('❌ 网关服务启动超时');
}
async function waitForFrontendReady(): Promise<void> {
const maxRetries = 90;
const retryInterval = 1000;
for (let i = 0; i < maxRetries; i++) {
renderProgressBar('⏳ 前端服务启动中', i, maxRetries);
try {
const response = await fetch('http://localhost:3002', {
signal: AbortSignal.timeout(5000) as any
});
if (response.ok) {
process.stdout.write('\r' + ' '.repeat(80) + '\r');
console.log(`✅ 前端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
return;
}
} catch (error) {
// 服务还未就绪,继续等待
}
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryInterval));
}
}
throw new Error('❌ 前端服务启动超时');
}
async function cleanupTestData(): Promise<void> {
try {
// 登录获取token
const loginResponse = await fetch('http://localhost:8084/api/auth/login', {
// 登录获取token(通过网关)
const loginResponse = await fetch('http://localhost:8080/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: 'admin',
password: 'admin123'
password: 'Test@123'
})
});
if (!loginResponse.ok) {
console.log('⚠️ 无法登录,跳过数据清理');
return;
}
const loginData = await loginResponse.json();
const token = loginData.token;
// 获取所有用户
const usersResponse = await fetch('http://localhost:8084/api/users', {
const usersResponse = await fetch('http://localhost:8080/api/users', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (usersResponse.ok) {
const users = await usersResponse.json();
// 删除测试创建的用户(保留ID 1-10的初始用户)
for (const user of users) {
if (user.id > 10) {
try {
await fetch(`http://localhost:8084/api/users/${user.id}`, {
await fetch(`http://localhost:8080/api/users/${user.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
@@ -200,22 +466,22 @@ async function cleanupTestData(): Promise<void> {
}
}
}
// 获取所有角色
const rolesResponse = await fetch('http://localhost:8084/api/roles', {
const rolesResponse = await fetch('http://localhost:8080/api/roles', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (rolesResponse.ok) {
const roles = await rolesResponse.json();
// 删除测试创建的角色(保留ID 1-4的初始角色)
for (const role of roles) {
if (role.id > 4) {
try {
await fetch(`http://localhost:8084/api/roles/${role.id}`, {
await fetch(`http://localhost:8080/api/roles/${role.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
@@ -228,7 +494,7 @@ async function cleanupTestData(): Promise<void> {
}
}
}
console.log('✅ 测试数据清理完成');
} catch (error) {
console.log('⚠️ 数据清理失败,继续执行测试');
@@ -238,20 +504,20 @@ async function cleanupTestData(): Promise<void> {
async function globalTeardown() {
console.log('🧹 开始全局测试环境清理...');
stopHealthMonitoring();
if (backendProcess) {
console.log('🛑 停止后端服务...');
backendProcess.kill('SIGTERM');
await new Promise<void>((resolve) => {
if (backendProcess) {
backendProcess.on('exit', () => {
console.log('✅ 后端服务已停止');
resolve();
});
setTimeout(() => {
if (backendProcess) {
backendProcess.kill('SIGKILL');
@@ -264,7 +530,31 @@ async function globalTeardown() {
}
});
}
if (gatewayProcess) {
console.log('🛑 停止网关服务...');
gatewayProcess.kill('SIGTERM');
await new Promise<void>((resolve) => {
if (gatewayProcess) {
gatewayProcess.on('exit', () => {
console.log('✅ 网关服务已停止');
resolve();
});
setTimeout(() => {
if (gatewayProcess) {
gatewayProcess.kill('SIGKILL');
console.log('⚠️ 强制停止网关服务');
resolve();
}
}, 10000);
} else {
resolve();
}
});
}
console.log('✅ 全局测试环境清理完成');
}
+23
View File
@@ -0,0 +1,23 @@
import { Page } from '@playwright/test';
export async function loginAsAdmin(page: Page) {
await page.goto('/login');
await page.waitForLoadState('networkidle');
await page.locator('input[placeholder*="用户名"]').fill('admin');
await page.locator('input[placeholder*="密码"]').fill('Test@123');
await page.locator('button:has-text("登录")').click();
await page.waitForURL('**/dashboard', { timeout: 30000 });
const token = await page.evaluate(() => {
return localStorage.getItem('token') || '';
});
return token;
}
export async function saveAuthState(page: Page) {
const storage = await page.context().storageState();
return storage;
}
@@ -1,110 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { UserManagementPage } from './pages/UserManagementPage';
test.describe('集成测试诊断', () => {
let loginPage: LoginPage;
let userManagementPage: UserManagementPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
userManagementPage = new UserManagementPage(page);
// 确保页面已经导航到正确的URL,避免localStorage访问错误
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
});
test('测试1: 登录并查询用户列表', async ({ page }) => {
console.log('=== 测试1: 登录并查询用户列表 ===');
await loginPage.goto();
await loginPage.login('admin', 'admin123');
const currentUrl = page.url();
console.log('当前URL:', currentUrl);
const token = await page.evaluate(() => localStorage.getItem('token'));
console.log('Token:', token ? '存在' : '不存在');
await userManagementPage.goto();
await userManagementPage.waitForTableReady();
const userCount = await userManagementPage.getUserCount();
console.log('用户数量:', userCount);
expect(userCount).toBeGreaterThan(0);
console.log('✅ 测试1通过\n');
});
test('测试2: 再次登录并创建用户', async ({ page }) => {
console.log('=== 测试2: 再次登录并创建用户 ===');
// 检查localStorage状态
const tokenBefore = await page.evaluate(() => localStorage.getItem('token'));
console.log('测试前Token:', tokenBefore ? '存在' : '不存在');
await loginPage.goto();
console.log('导航到登录页面');
const urlAfterGoto = page.url();
console.log('导航后URL:', urlAfterGoto);
// 如果已经有token,应该会自动跳转
if (tokenBefore) {
console.log('检测到已有token,等待自动跳转...');
await page.waitForTimeout(3000);
const urlAfterWait = page.url();
console.log('等待后URL:', urlAfterWait);
}
await loginPage.login('admin', 'admin123');
const currentUrl = page.url();
console.log('登录后URL:', currentUrl);
const tokenAfter = await page.evaluate(() => localStorage.getItem('token'));
console.log('登录后Token:', tokenAfter ? '存在' : '不存在');
await userManagementPage.goto();
await userManagementPage.waitForTableReady();
const uuid = Math.random().toString(36).substring(2, 15);
const username = `test_${uuid}`;
await userManagementPage.clickCreateUser();
await userManagementPage.fillUserForm({
username: username,
password: 'admin123',
email: `${username}@test.com`,
phone: '13800138000',
nickname: `测试用户${Date.now()}`
});
await userManagementPage.submitForm();
const success = await userManagementPage.waitForSuccessMessage();
console.log('创建用户:', success ? '成功' : '失败');
expect(success).toBeTruthy();
console.log('✅ 测试2通过\n');
});
test('测试3: 第三次登录', async ({ page }) => {
console.log('=== 测试3: 第三次登录 ===');
const tokenBefore = await page.evaluate(() => localStorage.getItem('token'));
console.log('测试前Token:', tokenBefore ? '存在' : '不存在');
await loginPage.goto();
await loginPage.login('admin', 'admin123');
const currentUrl = page.url();
console.log('登录后URL:', currentUrl);
const tokenAfter = await page.evaluate(() => localStorage.getItem('token'));
console.log('登录后Token:', tokenAfter ? '存在' : '不存在');
expect(currentUrl).not.toContain('/login');
console.log('✅ 测试3通过\n');
});
});
@@ -0,0 +1,203 @@
import { test, expect } from '@playwright/test';
test.describe('管理员完整工作流', () => {
test.describe.configure({ mode: 'serial' });
const timestamp = Date.now();
const roleName = `测试角色_${timestamp}`;
const roleKey = `test_role_${timestamp}`;
const username = `testuser_${timestamp}`;
test('创建角色并分配权限', async ({ page }) => {
await test.step('导航到角色管理', async () => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await page.locator('text=系统管理').click();
await page.waitForTimeout(500);
await page.locator('text=角色管理').click();
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/.*roles/, { timeout: 10000 });
});
await test.step('点击创建角色按钮', async () => {
await page.locator('button:has-text("新增角色")').click();
await page.waitForSelector('.el-dialog', { state: 'visible', timeout: 5000 });
});
await test.step('填写角色信息', async () => {
const dialog = page.locator('.el-dialog');
await dialog.locator('input').first().fill(roleName);
await dialog.locator('input').nth(1).fill(roleKey);
await dialog.locator('.el-input-number .el-input__inner').fill('99');
});
await test.step('提交表单', async () => {
await page.locator('.el-dialog button:has-text("确定")').click();
await page.waitForSelector('.el-dialog', { state: 'hidden', timeout: 10000 });
await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 });
});
});
test('创建用户并分配角色', async ({ page }) => {
await test.step('导航到用户管理', async () => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await page.locator('text=系统管理').click();
await page.waitForTimeout(500);
await page.locator('text=用户管理').click();
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/.*users/, { timeout: 10000 });
});
await test.step('点击创建用户按钮', async () => {
await page.locator('button:has-text("新增用户")').click();
await page.waitForSelector('.el-dialog', { state: 'visible', timeout: 5000 });
});
await test.step('填写用户信息', async () => {
const dialog = page.locator('.el-dialog');
await dialog.locator('input').first().fill(username);
await dialog.locator('input[type="password"]').fill('Test@123');
await dialog.locator('input').nth(2).fill(`测试用户${timestamp}`);
await dialog.locator('input').nth(3).fill(`test_${timestamp}@example.com`);
await dialog.locator('input').nth(4).fill('13800138000');
});
await test.step('提交表单', async () => {
await page.locator('.el-dialog button:has-text("确定")').click();
await page.waitForSelector('.el-dialog', { state: 'hidden', timeout: 10000 });
await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 });
});
await test.step('搜索新创建的用户', async () => {
await page.waitForTimeout(1000);
const searchInput = page.locator('input[placeholder*="搜索"]');
await searchInput.waitFor({ state: 'visible', timeout: 5000 });
await searchInput.fill(username);
await page.locator('button:has-text("搜索")').click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
});
await test.step('分配角色', async () => {
const userRow = page.locator(`tr:has-text("${username}")`);
await expect(userRow).toBeVisible({ timeout: 10000 });
await userRow.locator('button:has-text("分配角色")').click();
await page.waitForSelector('.el-dialog:has-text("分配角色")', { state: 'visible', timeout: 5000 });
const transfer = page.locator('.el-transfer');
const leftPanel = transfer.locator('.el-transfer-panel').first();
const rightPanel = transfer.locator('.el-transfer-panel').last();
const rightPanelItems = await rightPanel.locator('.el-checkbox').all();
let hasSuperAdminRole = false;
for (const item of rightPanelItems) {
const text = await item.textContent();
if (text?.includes('超级管理员')) {
hasSuperAdminRole = true;
break;
}
}
if (!hasSuperAdminRole) {
const leftPanelItems = await leftPanel.locator('.el-checkbox').all();
let superAdminCheckbox = null;
for (const item of leftPanelItems) {
const text = await item.textContent();
if (text?.includes('超级管理员')) {
superAdminCheckbox = item;
break;
}
}
if (superAdminCheckbox) {
const isChecked = await superAdminCheckbox.locator('input').isChecked();
if (!isChecked) {
await superAdminCheckbox.click();
await page.waitForTimeout(500);
}
const moveToRightButton = transfer.locator('.el-transfer__buttons button').nth(1);
if (await moveToRightButton.isEnabled()) {
await moveToRightButton.click();
await page.waitForTimeout(500);
}
}
}
await page.locator('.el-dialog:has-text("分配角色") button:has-text("确定")').click();
await page.waitForSelector('.el-dialog:has-text("分配角色")', { state: 'hidden', timeout: 10000 });
await expect(page.locator('.el-message--success').last()).toBeVisible({ timeout: 5000 });
});
});
test('验证新用户登录', async ({ page }) => {
await test.step('管理员登出', async () => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
const avatarButton = page.locator('.el-avatar').first();
await avatarButton.click({ timeout: 10000 });
await page.waitForTimeout(500);
await page.locator('text=退出登录').click();
await page.waitForURL(/.*login/, { timeout: 10000 });
});
await test.step('新用户登录', async () => {
await page.goto('/login');
await page.locator('input[placeholder*="用户名"]').fill(username);
await page.locator('input[placeholder*="密码"]').fill('Test@123');
await page.locator('button:has-text("登录")').click();
await page.waitForURL('**/dashboard', { timeout: 30000 });
});
await test.step('验证用户已登录', async () => {
await expect(page).toHaveURL(/.*dashboard/);
});
});
test.skip('清理测试数据', async ({ page }) => {
await test.step('管理员重新登录', async () => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
const avatarButton = page.locator('.el-avatar').first();
if (await avatarButton.isVisible()) {
await avatarButton.click();
await page.waitForTimeout(500);
await page.locator('text=退出登录').click();
}
await page.goto('/login');
await page.locator('input[placeholder*="用户名"]').fill('admin');
await page.locator('input[placeholder*="密码"]').fill('Test@123');
await page.locator('button:has-text("登录")').click();
await page.waitForURL('**/dashboard');
});
await test.step('删除测试用户', async () => {
await page.goto('/users');
await page.locator('input[placeholder*="搜索"]').fill(username);
await page.locator('button:has-text("搜索")').click();
await page.waitForTimeout(1000);
await page.locator('button:has-text("删除")').first().click();
await page.locator('button:has-text("确定")').click();
await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 });
});
await test.step('删除测试角色', async () => {
await page.goto('/roles');
await page.locator('input[placeholder*="搜索"]').fill(roleName);
await page.locator('button:has-text("搜索")').click();
await page.waitForTimeout(1000);
await page.locator('button:has-text("删除")').first().click();
await page.locator('button:has-text("确定")').click();
await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 });
});
});
});
@@ -0,0 +1,106 @@
import { test, expect } from '@playwright/test';
test.describe('审计工作流', () => {
test('执行操作并查看操作日志', async ({ page }) => {
await test.step('执行用户管理操作', async () => {
await page.goto('/users');
await page.waitForTimeout(1000);
});
await test.step('执行角色管理操作', async () => {
await page.goto('/roles');
await page.waitForTimeout(1000);
});
await test.step('执行菜单管理操作', async () => {
await page.goto('/menus');
await page.waitForTimeout(1000);
});
await test.step('导航到操作日志', async () => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await page.locator('text=审计日志').click();
await page.waitForTimeout(1000);
await page.locator('.el-menu-item:has-text("操作日志")').click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await expect(page).toHaveURL(/.*oplog/, { timeout: 10000 });
await expect(page.locator('.el-table')).toBeVisible({ timeout: 10000 });
});
await test.step('验证操作日志记录', async () => {
await page.waitForTimeout(2000);
const logContent = await page.locator('.el-table').textContent();
expect(logContent).toMatch(/用户管理|角色管理|菜单管理/);
});
});
test('查看登录日志', async ({ page }) => {
await test.step('导航到登录日志', async () => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await page.locator('text=审计日志').click();
await page.waitForTimeout(1000);
await page.locator('.el-menu-item:has-text("登录日志")').click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await expect(page).toHaveURL(/.*loginlog/, { timeout: 10000 });
});
await test.step('验证登录日志显示', async () => {
await expect(page.locator('.el-table')).toBeVisible({ timeout: 10000 });
const logContent = await page.locator('.el-table').textContent();
expect(logContent).toBeTruthy();
expect(logContent.length).toBeGreaterThan(0);
});
});
test('搜索和筛选日志', async ({ page }) => {
await test.step('导航到操作日志', async () => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await page.locator('text=审计日志').click();
await page.waitForTimeout(1000);
await page.locator('.el-menu-item:has-text("操作日志")').click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await expect(page.locator('.el-table')).toBeVisible({ timeout: 10000 });
});
await test.step('按模块筛选', async () => {
const moduleSelect = page.locator('.el-select:has-text("模块")');
if (await moduleSelect.isVisible()) {
await moduleSelect.click();
await page.locator('.el-select-dropdown__item:has-text("用户管理")').click();
await page.waitForTimeout(1000);
}
});
await test.step('按时间范围筛选', async () => {
const dateRangePicker = page.locator('.el-date-editor');
if (await dateRangePicker.isVisible()) {
await dateRangePicker.click();
await page.waitForTimeout(500);
}
});
await test.step('搜索特定内容', async () => {
const searchInput = page.locator('input[placeholder*="搜索"]');
if (await searchInput.isVisible()) {
await searchInput.fill('admin');
await page.locator('button:has-text("搜索")').click();
await page.waitForTimeout(1000);
}
});
});
});
@@ -0,0 +1,140 @@
import { test, expect } from '@playwright/test';
import { SystemConfigPage } from '../pages/SystemConfigPage';
test.describe('系统配置工作流', () => {
let configPage: SystemConfigPage;
const timestamp = Date.now();
const configKey = `test_config_${timestamp}`;
const configName = `测试配置_${timestamp}`;
const configValue = `测试值_${timestamp}`;
test.beforeEach(async ({ page }) => {
configPage = new SystemConfigPage(page);
});
test('查看系统配置列表', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('验证表格显示', async () => {
await expect(configPage.table).toBeVisible({ timeout: 10000 });
});
await test.step('验证数据加载', async () => {
const rowCount = await configPage.getTableRowCount();
console.log(`系统配置列表包含 ${rowCount} 条记录`);
expect(rowCount).toBeGreaterThanOrEqual(0);
});
});
test('新增系统配置', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('点击新增配置按钮', async () => {
await configPage.addButton.click();
await configPage.dialog.waitFor({ state: 'visible', timeout: 5000 });
});
await test.step('填写配置表单', async () => {
await configPage.configNameInput.fill(configName);
await configPage.configKeyInput.fill(configKey);
await configPage.configValueInput.fill(configValue);
});
await test.step('提交表单', async () => {
await configPage.saveButton.click();
await page.waitForLoadState('networkidle');
});
await test.step('验证创建成功', async () => {
await expect(configPage.dialog).not.toBeVisible({ timeout: 5000 });
console.log(`配置 ${configName} 创建完成`);
});
});
test('编辑系统配置', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('等待数据加载', async () => {
await expect(configPage.table).toBeVisible({ timeout: 10000 });
});
await test.step('点击编辑按钮', async () => {
const rows = await configPage.getTableRowCount();
if (rows > 0) {
const firstRow = configPage.table.locator('tr').first();
const editBtn = firstRow.getByRole('button', { name: '编辑' });
if (await editBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await editBtn.click();
await configPage.dialog.waitFor({ state: 'visible', timeout: 5000 });
await test.step('修改配置值', async () => {
const newValue = `更新值_${timestamp}`;
await configPage.configValueInput.clear();
await configPage.configValueInput.fill(newValue);
});
await test.step('提交表单', async () => {
await configPage.saveButton.click();
await page.waitForLoadState('networkidle');
});
await test.step('验证更新成功', async () => {
await expect(configPage.dialog).not.toBeVisible({ timeout: 5000 });
console.log(`配置已更新`);
});
} else {
console.log('未找到编辑按钮,跳过编辑测试');
}
} else {
console.log('当前没有配置记录,跳过编辑测试');
}
});
});
test('删除系统配置', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('等待数据加载', async () => {
await expect(configPage.table).toBeVisible({ timeout: 10000 });
});
await test.step('点击删除按钮', async () => {
const rows = await configPage.getTableRowCount();
if (rows > 0) {
const firstRow = configPage.table.locator('tr').first();
const deleteBtn = firstRow.getByRole('button', { name: '删除' });
if (await deleteBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await deleteBtn.click();
const confirmBtn = page.locator('.el-message-box');
await confirmBtn.waitFor({ state: 'visible', timeout: 3000 });
await test.step('确认删除', async () => {
const confirmBtn = page.locator('.el-message-box').getByRole('button', { name: '确定' });
if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
await confirmBtn.click();
await page.waitForLoadState('networkidle');
}
});
await test.step('验证删除成功', async () => {
const messageBox = page.locator('.el-message-box');
await expect(messageBox).not.toBeVisible({ timeout: 5000 });
console.log(`配置已删除`);
});
} else {
console.log('未找到删除按钮,跳过删除测试');
}
} else {
console.log('当前没有配置记录,跳过删除测试');
}
});
});
});
@@ -0,0 +1,138 @@
import { test, expect } from '@playwright/test';
import { DictionaryManagementPage } from '../pages/DictionaryManagementPage';
test.describe('字典管理工作流', () => {
let dictPage: DictionaryManagementPage;
const timestamp = Date.now();
const dictType = `test_dict_${timestamp}`;
const dictName = `测试字典_${timestamp}`;
test.beforeEach(async ({ page }) => {
dictPage = new DictionaryManagementPage(page);
});
test('查看字典列表', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictPage.goto();
});
await test.step('验证表格显示', async () => {
await expect(dictPage.table).toBeVisible({ timeout: 10000 });
});
await test.step('验证数据加载', async () => {
const rowCount = await dictPage.getDictCount();
console.log(`字典列表包含 ${rowCount} 条记录`);
expect(rowCount).toBeGreaterThanOrEqual(0);
});
});
test('新增字典', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictPage.goto();
});
await test.step('点击新增字典按钮', async () => {
await dictPage.createDictButton.click();
await dictPage.dialog.waitFor({ state: 'visible', timeout: 5000 });
});
await test.step('填写字典表单', async () => {
await dictPage.dictNameInput.fill(dictName);
await dictPage.dictTypeInput.fill(dictType);
});
await test.step('提交表单', async () => {
await dictPage.saveButton.click();
await page.waitForLoadState('networkidle');
});
await test.step('验证创建成功', async () => {
await expect(dictPage.dialog).not.toBeVisible({ timeout: 5000 });
console.log(`字典 ${dictName} 创建完成`);
});
});
test('编辑字典', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictPage.goto();
});
await test.step('等待数据加载', async () => {
await expect(dictPage.table).toBeVisible({ timeout: 10000 });
});
await test.step('点击编辑按钮', async () => {
const rows = await dictPage.getDictCount();
if (rows > 0) {
const firstRow = dictPage.table.locator('tr').first();
const editBtn = firstRow.getByRole('button', { name: '编辑' });
if (await editBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await editBtn.click();
await dictPage.dialog.waitFor({ state: 'visible', timeout: 5000 });
await test.step('修改字典名称', async () => {
const newName = `更新字典_${timestamp}`;
await dictPage.dictNameInput.clear();
await dictPage.dictNameInput.fill(newName);
});
await test.step('提交表单', async () => {
await dictPage.saveButton.click();
await page.waitForLoadState('networkidle');
});
await test.step('验证更新成功', async () => {
await expect(dictPage.dialog).not.toBeVisible({ timeout: 5000 });
console.log(`字典已更新`);
});
} else {
console.log('未找到编辑按钮,跳过编辑测试');
}
} else {
console.log('当前没有字典记录,跳过编辑测试');
}
});
});
test('删除字典', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await dictPage.goto();
});
await test.step('等待数据加载', async () => {
await expect(dictPage.table).toBeVisible({ timeout: 10000 });
});
await test.step('点击删除按钮', async () => {
const rows = await dictPage.getDictCount();
if (rows > 0) {
const firstRow = dictPage.table.locator('tr').first();
const deleteBtn = firstRow.getByRole('button', { name: '删除' });
if (await deleteBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await deleteBtn.click();
const confirmBtn = page.locator('.el-message-box');
await confirmBtn.waitFor({ state: 'visible', timeout: 3000 });
await test.step('确认删除', async () => {
const confirmBtn = page.locator('.el-message-box').getByRole('button', { name: '确定' });
if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
await confirmBtn.click();
await page.waitForLoadState('networkidle');
}
});
await test.step('验证删除成功', async () => {
const messageBox = page.locator('.el-message-box');
await expect(messageBox).not.toBeVisible({ timeout: 5000 });
console.log(`字典已删除`);
});
} else {
console.log('未找到删除按钮,跳过删除测试');
}
} else {
console.log('当前没有字典记录,跳过删除测试');
}
});
});
});
@@ -0,0 +1,72 @@
import { test, expect } from '@playwright/test';
import { ExceptionLogPage } from '../pages/ExceptionLogPage';
test.describe('异常日志工作流', () => {
let exceptionLogPage: ExceptionLogPage;
test.beforeEach(async ({ page }) => {
exceptionLogPage = new ExceptionLogPage(page);
});
test('查看异常日志列表', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('验证表格显示', async () => {
await expect(exceptionLogPage.table).toBeVisible({ timeout: 10000 });
});
await test.step('验证数据加载', async () => {
const rowCount = await exceptionLogPage.getLogCount();
console.log(`异常日志列表包含 ${rowCount} 条记录`);
expect(rowCount).toBeGreaterThanOrEqual(0);
});
});
test('搜索异常日志', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('输入搜索关键词', async () => {
const searchKeyword = 'NullPointerException';
await exceptionLogPage.search(searchKeyword);
});
await test.step('验证搜索结果', async () => {
await page.waitForLoadState('networkidle');
const rowCount = await exceptionLogPage.getLogCount();
console.log(`搜索结果包含 ${rowCount} 条记录`);
});
});
test('查看异常日志详情', async ({ page }) => {
await test.step('导航到异常日志页面', async () => {
await exceptionLogPage.goto();
});
await test.step('等待数据加载', async () => {
await expect(exceptionLogPage.table).toBeVisible({ timeout: 10000 });
});
await test.step('点击查看详情按钮', async () => {
const detailButton = page.locator('button:has-text("详情")').or(page.locator('.detail-button')).first();
if (await detailButton.isVisible({ timeout: 3000 }).catch(() => false)) {
await detailButton.click();
await test.step('验证详情对话框显示', async () => {
const dialog = page.locator('.el-dialog');
await expect(dialog).toBeVisible({ timeout: 5000 });
console.log('异常日志详情对话框已打开');
});
await test.step('关闭详情对话框', async () => {
await exceptionLogPage.closeDetailDialog();
});
} else {
console.log('当前没有异常日志记录,跳过详情查看测试');
}
});
});
});
@@ -0,0 +1,89 @@
import { test, expect } from '@playwright/test';
test.describe('文件管理工作流', () => {
test('文件上传流程', async ({ page }) => {
await test.step('导航到文件管理', async () => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await page.locator('text=系统管理').click();
await page.waitForTimeout(500);
await page.locator('.el-menu-item:has-text("文件管理")').click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await expect(page.locator('.el-table')).toBeVisible({ timeout: 10000 });
});
await test.step('上传文件', async () => {
const uploadButton = page.locator('button:has-text("上传")');
if (await uploadButton.isVisible()) {
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles({
name: 'test-file.txt',
mimeType: 'text/plain',
buffer: Buffer.from('Test file content'),
});
await page.waitForTimeout(2000);
}
});
await test.step('验证文件上传成功', async () => {
const successMessage = page.locator('.el-message--success');
if (await successMessage.isVisible()) {
expect(await successMessage.textContent()).toContain('成功');
}
});
});
test('文件搜索和筛选', async ({ page }) => {
await test.step('导航到文件管理', async () => {
await page.goto('/dashboard');
await page.locator('text=系统管理').click();
await page.locator('text=文件管理').click();
});
await test.step('搜索文件', async () => {
const searchInput = page.locator('input[placeholder*="搜索"]');
if (await searchInput.isVisible()) {
await searchInput.fill('test');
await page.locator('button:has-text("搜索")').click();
await page.waitForTimeout(1000);
}
});
await test.step('按类型筛选', async () => {
const typeFilter = page.locator('.el-select:has-text("类型")');
if (await typeFilter.isVisible()) {
await typeFilter.click();
await page.locator('.el-select-dropdown__item').first().click();
await page.waitForTimeout(1000);
}
});
});
test('文件删除流程', async ({ page }) => {
await test.step('导航到文件管理', async () => {
await page.goto('/dashboard');
await page.locator('text=系统管理').click();
await page.locator('text=文件管理').click();
});
await test.step('选择文件', async () => {
const fileCheckbox = page.locator('.el-checkbox').first();
if (await fileCheckbox.isVisible()) {
await fileCheckbox.click();
}
});
await test.step('删除文件', async () => {
const deleteButton = page.locator('button:has-text("删除")');
if (await deleteButton.isVisible()) {
await deleteButton.click();
await page.locator('button:has-text("确定")').click();
await page.waitForTimeout(1000);
}
});
});
});
@@ -0,0 +1,138 @@
import { test, expect } from '@playwright/test';
import { NotificationPage } from '../pages/NotificationPage';
test.describe('通知管理工作流', () => {
let noticePage: NotificationPage;
const timestamp = Date.now();
const noticeTitle = `测试通知_${timestamp}`;
const noticeContent = `这是测试通知内容_${timestamp}`;
test.beforeEach(async ({ page }) => {
noticePage = new NotificationPage(page);
});
test('查看通知列表', async ({ page }) => {
await test.step('导航到通知管理页面', async () => {
await noticePage.goto();
});
await test.step('验证表格显示', async () => {
await expect(noticePage.table).toBeVisible({ timeout: 10000 });
});
await test.step('验证数据加载', async () => {
const rowCount = await noticePage.getTableRowCount();
console.log(`通知列表包含 ${rowCount} 条记录`);
expect(rowCount).toBeGreaterThanOrEqual(0);
});
});
test('新增通知', async ({ page }) => {
await test.step('导航到通知管理页面', async () => {
await noticePage.goto();
});
await test.step('点击新增通知按钮', async () => {
await noticePage.addButton.click();
await noticePage.dialog.waitFor({ state: 'visible', timeout: 5000 });
});
await test.step('填写通知表单', async () => {
await noticePage.titleInput.fill(noticeTitle);
await noticePage.contentInput.fill(noticeContent);
});
await test.step('提交表单', async () => {
await noticePage.saveButton.click();
await page.waitForLoadState('networkidle');
});
await test.step('验证创建成功', async () => {
await expect(noticePage.dialog).not.toBeVisible({ timeout: 5000 });
console.log(`通知 ${noticeTitle} 创建完成`);
});
});
test('编辑通知', async ({ page }) => {
await test.step('导航到通知管理页面', async () => {
await noticePage.goto();
});
await test.step('等待数据加载', async () => {
await expect(noticePage.table).toBeVisible({ timeout: 10000 });
});
await test.step('点击编辑按钮', async () => {
const rows = await noticePage.getTableRowCount();
if (rows > 0) {
const firstRow = noticePage.table.locator('tr').first();
const editBtn = firstRow.getByRole('button', { name: '编辑' });
if (await editBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await editBtn.click();
await noticePage.dialog.waitFor({ state: 'visible', timeout: 5000 });
await test.step('修改通知内容', async () => {
const newContent = `更新通知内容_${timestamp}`;
await noticePage.contentInput.clear();
await noticePage.contentInput.fill(newContent);
});
await test.step('提交表单', async () => {
await noticePage.saveButton.click();
await page.waitForLoadState('networkidle');
});
await test.step('验证更新成功', async () => {
await expect(noticePage.dialog).not.toBeVisible({ timeout: 5000 });
console.log(`通知已更新`);
});
} else {
console.log('未找到编辑按钮,跳过编辑测试');
}
} else {
console.log('当前没有通知记录,跳过编辑测试');
}
});
});
test('删除通知', async ({ page }) => {
await test.step('导航到通知管理页面', async () => {
await noticePage.goto();
});
await test.step('等待数据加载', async () => {
await expect(noticePage.table).toBeVisible({ timeout: 10000 });
});
await test.step('点击删除按钮', async () => {
const rows = await noticePage.getTableRowCount();
if (rows > 0) {
const firstRow = noticePage.table.locator('tr').first();
const deleteBtn = firstRow.getByRole('button', { name: '删除' });
if (await deleteBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await deleteBtn.click();
const confirmBtn = page.locator('.el-message-box');
await confirmBtn.waitFor({ state: 'visible', timeout: 3000 });
await test.step('确认删除', async () => {
const confirmBtn = page.locator('.el-message-box').getByRole('button', { name: '确定' });
if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
await confirmBtn.click();
await page.waitForLoadState('networkidle');
}
});
await test.step('验证删除成功', async () => {
const messageBox = page.locator('.el-message-box');
await expect(messageBox).not.toBeVisible({ timeout: 5000 });
console.log(`通知已删除`);
});
} else {
console.log('未找到删除按钮,跳过删除测试');
}
} else {
console.log('当前没有通知记录,跳过删除测试');
}
});
});
});
@@ -0,0 +1,116 @@
import { test, expect } from '@playwright/test';
test.describe('用户权限边界验证', () => {
test('管理员可以访问所有管理功能', async ({ page }) => {
await test.step('验证可以访问用户管理', async () => {
await page.goto('/users');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/.*users/);
await expect(page.locator('.el-table')).toBeVisible({ timeout: 10000 });
});
await test.step('验证可以访问角色管理', async () => {
await page.goto('/roles');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/.*roles/);
await expect(page.locator('.el-table')).toBeVisible({ timeout: 10000 });
});
await test.step('验证可以访问菜单管理', async () => {
await page.goto('/menus');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/.*menus/);
await expect(page.locator('.el-table')).toBeVisible({ timeout: 10000 });
});
});
test('普通用户登录后可以访问页面但API操作受限', async ({ page }) => {
await test.step('管理员登出', async () => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
const avatarButton = page.locator('.el-avatar').first();
await avatarButton.click({ timeout: 10000 });
await page.waitForTimeout(500);
await page.locator('text=退出登录').click();
await page.waitForURL(/.*login/, { timeout: 10000 });
});
await test.step('普通用户登录', async () => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
const usernameInput = page.locator('input[placeholder*="用户名"]');
const passwordInput = page.locator('input[placeholder*="密码"]');
const loginButton = page.locator('button:has-text("登录")');
await usernameInput.waitFor({ state: 'visible' });
await usernameInput.fill('user');
await passwordInput.waitFor({ state: 'visible' });
await passwordInput.fill('Test@123');
await loginButton.waitFor({ state: 'visible' });
await loginButton.click();
await page.waitForURL('**/dashboard', { timeout: 30000 });
});
await test.step('验证普通用户可以访问用户管理页面', async () => {
await page.goto('/users');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/.*users/);
});
await test.step('验证普通用户无法创建用户', async () => {
const createButton = page.locator('button:has-text("新增用户")');
if (await createButton.isVisible()) {
await createButton.click();
await page.waitForTimeout(2000);
const errorMessage = page.locator('.el-message--error');
const hasError = await errorMessage.isVisible().catch(() => false);
expect(hasError || await page.locator('.el-dialog').isVisible()).toBeTruthy();
}
});
});
test('权限不足时API返回403错误', async ({ page }) => {
await test.step('管理员登出', async () => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
const avatarButton = page.locator('.el-avatar').first();
await avatarButton.click({ timeout: 10000 });
await page.waitForTimeout(500);
await page.locator('text=退出登录').click();
await page.waitForURL(/.*login/, { timeout: 10000 });
});
await test.step('普通用户登录', async () => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
const usernameInput = page.locator('input[placeholder*="用户名"]');
const passwordInput = page.locator('input[placeholder*="密码"]');
const loginButton = page.locator('button:has-text("登录")');
await usernameInput.waitFor({ state: 'visible' });
await usernameInput.fill('user');
await passwordInput.waitFor({ state: 'visible' });
await passwordInput.fill('Test@123');
await loginButton.waitFor({ state: 'visible' });
await loginButton.click();
await page.waitForURL('**/dashboard', { timeout: 30000 });
});
await test.step('尝试访问受限API', async () => {
const response = await page.request.get('/api/users?page=0&size=10');
expect([200, 401, 403]).toContain(response.status());
});
});
});
@@ -1,117 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('登录诊断测试', () => {
test('诊断登录流程', async ({ page }) => {
console.log('=== 开始诊断登录流程 ===');
// 导航到登录页面
await page.goto('/login');
console.log('1. 导航到登录页面');
// 等待页面加载完成
await page.waitForLoadState('networkidle');
console.log('2. 页面加载完成');
// 监听API响应
const [response] = await Promise.all([
page.waitForResponse(resp =>
resp.url().includes('/api/auth/login') &&
resp.request().method() === 'POST',
{ timeout: 15000 }
).catch(err => {
console.log(' ❌ 等待登录API响应超时:', err.message);
return null;
}),
(async () => {
// 填写登录表单
await page.fill('input[placeholder="请输入用户名"]', 'admin');
console.log('3. 填写用户名: admin');
await page.fill('input[placeholder="请输入密码"]', 'admin123');
console.log('4. 填写密码: admin123');
// 点击登录按钮
await page.click('button:has-text("登录")');
console.log('5. 点击登录按钮');
})()
]);
if (response) {
console.log(' ✅ 捕获到登录API响应');
console.log(' - 状态码:', response.status());
console.log(' - URL:', response.url());
try {
const responseBody = await response.json();
console.log(' - 响应体:', JSON.stringify(responseBody, null, 2));
// 检查响应格式
if (responseBody.token) {
console.log(' ✅ 响应包含token');
} else {
console.log(' ❌ 响应不包含token');
}
if (responseBody.userId) {
console.log(' ✅ 响应包含userId:', responseBody.userId);
} else {
console.log(' ⚠️ 响应不包含userId');
}
if (responseBody.username) {
console.log(' ✅ 响应包含username:', responseBody.username);
} else {
console.log(' ⚠️ 响应不包含username');
}
} catch (err) {
console.log(' ❌ 无法解析响应体:', err.message);
}
} else {
console.log(' ❌ 没有捕获到登录API响应');
}
// 等待一段时间,观察页面变化
await page.waitForTimeout(3000);
// 检查当前URL
const currentUrl = page.url();
console.log('6. 当前URL:', currentUrl);
// 检查localStorage中的token
const token = await page.evaluate(() => localStorage.getItem('token'));
console.log('7. Token in localStorage:', token ? '✅ 存在' : '❌ 不存在');
if (token) {
console.log(' - Token前20字符:', token.substring(0, 20));
}
// 检查localStorage中的userId
const userId = await page.evaluate(() => localStorage.getItem('userId'));
console.log('8. UserId in localStorage:', userId || '❌ 不存在');
// 检查localStorage中的username
const username = await page.evaluate(() => localStorage.getItem('username'));
console.log('9. Username in localStorage:', username || '❌ 不存在');
// 检查是否有错误消息
const errorMessages = await page.locator('.el-message--error').allTextContents();
if (errorMessages.length > 0) {
console.log(' ⚠️ 发现错误消息:', errorMessages);
}
// 检查成功消息
const successMessages = await page.locator('.el-message--success').allTextContents();
if (successMessages.length > 0) {
console.log(' ✅ 发现成功消息:', successMessages);
}
// 截图
await page.screenshot({ path: `test-results/login-diagnostic-${Date.now()}.png` });
console.log('10. 截图已保存');
console.log('=== 诊断完成 ===');
// 验证登录是否成功
expect(token).toBeTruthy();
expect(currentUrl).not.toContain('/login');
});
});
-166
View File
@@ -1,166 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { LoginLogPage } from './pages/LoginLogPage';
test.describe('登录日志E2E测试', () => {
let loginPage: LoginPage;
let loginLogPage: LoginLogPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
loginLogPage = new LoginLogPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
test.afterEach(async ({ page }) => {
await loginPage.logout();
});
test('登录日志页面导航', async ({ page }) => {
await test.step('导航到登录日志页面', async () => {
await loginLogPage.goto();
await expect(page).toHaveURL(/.*loginlog/);
});
await test.step('验证页面元素可见', async () => {
await expect(loginLogPage.table).toBeVisible();
await expect(loginLogPage.searchInput).toBeVisible();
await expect(loginLogPage.exportButton).toBeVisible();
});
});
test('搜索登录日志', async ({ page }) => {
await test.step('导航到登录日志页面', async () => {
await loginLogPage.goto();
});
await test.step('搜索登录日志', async () => {
const keyword = 'admin';
await loginLogPage.searchByKeyword(keyword);
await loginLogPage.verifyTableContains(keyword);
});
await test.step('清除搜索', async () => {
await loginLogPage.clearSearch();
const rowCount = await loginLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('登录日志分页功能', async ({ page }) => {
await test.step('导航到登录日志页面', async () => {
await loginLogPage.goto();
});
await test.step('验证表格数据加载', async () => {
const rowCount = await loginLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('登录日志响应式布局', async ({ page }) => {
await test.step('导航到登录日志页面', async () => {
await loginLogPage.goto();
});
await test.step('验证桌面端布局', async () => {
await page.setViewportSize({ width: 1280, height: 720 });
await expect(loginLogPage.table).toBeVisible();
await expect(loginLogPage.exportButton).toBeVisible();
});
await test.step('验证平板端布局', async () => {
await page.setViewportSize({ width: 768, height: 1024 });
await expect(loginLogPage.table).toBeVisible();
await expect(loginLogPage.exportButton).toBeVisible();
});
await test.step('验证移动端布局', async () => {
await page.setViewportSize({ width: 375, height: 667 });
await expect(loginLogPage.table).toBeVisible();
});
});
test('登录日志数据验证', async ({ page }) => {
await test.step('导航到登录日志页面', async () => {
await loginLogPage.goto();
});
await test.step('验证日志数据完整性', async () => {
const rowCount = await loginLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
await test.step('验证日志字段显示', async () => {
await expect(loginLogPage.table).toBeVisible();
});
});
test('登录日志搜索功能', async ({ page }) => {
await test.step('导航到登录日志页面', async () => {
await loginLogPage.goto();
});
await test.step('按用户名搜索', async () => {
const username = 'admin';
await loginLogPage.searchByKeyword(username);
await loginLogPage.verifyTableContains(username);
});
await test.step('按IP地址搜索', async () => {
const ipAddress = '127.0.0.1';
await loginLogPage.searchByKeyword(ipAddress);
});
await test.step('清除搜索结果', async () => {
await loginLogPage.clearSearch();
const rowCount = await loginLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('登录日志导出功能', async ({ page }) => {
await test.step('导航到登录日志页面', async () => {
await loginLogPage.goto();
});
await test.step('导出登录日志', async () => {
const downloadPromise = page.waitForEvent('download');
await loginLogPage.exportData();
const download = await downloadPromise;
expect(download).toBeDefined();
});
});
test('登录日志时间范围验证', async ({ page }) => {
await test.step('导航到登录日志页面', async () => {
await loginLogPage.goto();
});
await test.step('验证日志时间戳显示', async () => {
const rowCount = await loginLogPage.getTableRowCount();
if (rowCount > 0) {
await expect(loginLogPage.table).toBeVisible();
}
});
});
test('登录日志权限验证', async ({ page }) => {
await test.step('导航到登录日志页面', async () => {
await loginLogPage.goto();
});
await test.step('验证导出按钮可见性', async () => {
await expect(loginLogPage.exportButton).toBeVisible();
});
await test.step('验证搜索功能可用', async () => {
await expect(loginLogPage.searchInput).toBeVisible();
await expect(loginLogPage.searchButton).toBeVisible();
});
});
});
@@ -1,35 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test.describe('登录稳定性测试', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
// 确保页面已经导航到正确的URL,避免localStorage访问错误
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
});
// 连续执行10次登录测试,验证稳定性
for (let i = 1; i <= 10; i++) {
test(`登录测试 #${i}`, async ({ page }) => {
console.log(`=== 开始登录测试 #${i} ===`);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
const currentUrl = page.url();
console.log(`测试 #${i} - 当前URL:`, currentUrl);
const token = await page.evaluate(() => localStorage.getItem('token'));
console.log(`测试 #${i} - Token:`, token ? '存在' : '不存在');
expect(currentUrl).not.toContain('/login');
expect(token).toBeTruthy();
console.log(`✅ 测试 #${i} 通过\n`);
});
}
});
-34
View File
@@ -1,34 +0,0 @@
import { test, expect } from '@playwright/test'
test.describe('登录签名测试', () => {
test('登录功能应该正常工作', async ({ page }) => {
page.on('console', msg => {
console.log('BROWSER CONSOLE:', msg.type(), msg.text())
})
page.on('pageerror', error => {
console.error('PAGE ERROR:', error.message)
})
page.on('requestfailed', request => {
console.error('REQUEST FAILED:', request.url(), request.failure()?.errorText)
})
await page.goto('/login')
await page.fill('input[placeholder="请输入用户名"]', 'admin')
await page.fill('input[placeholder="请输入密码"]', 'admin123')
await page.click('button:has-text("登录")')
await page.waitForURL('**/dashboard', { timeout: 10000 })
console.log('Current URL after login:', page.url())
const token = await page.evaluate(() => localStorage.getItem('token'))
console.log('Token in localStorage:', token ? 'exists' : 'not found')
expect(page.url()).toContain('/dashboard')
expect(token).toBeTruthy()
})
})
@@ -1,400 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { MenuManagementPage } from './pages/MenuManagementPage';
test.describe('菜单管理 E2E 测试', () => {
let loginPage: LoginPage;
let menuManagementPage: MenuManagementPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
menuManagementPage = new MenuManagementPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
test('MENU-001: 访问菜单管理页面', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
await expect(page).toHaveURL(/.*menus/);
});
await test.step('验证页面元素可见', async () => {
await expect(menuManagementPage.table).toBeVisible();
await expect(menuManagementPage.createMenuButton).toBeVisible();
await expect(menuManagementPage.searchInput).toBeVisible();
});
});
test('MENU-002: 创建一级菜单', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('点击新增菜单按钮', async () => {
await menuManagementPage.clickCreateMenu();
});
await test.step('填写菜单信息', async () => {
const timestamp = Date.now();
const menuData = {
menuName: `测试菜单_${timestamp}`,
menuType: '目录',
path: `/test-menu-${timestamp}`,
sort: 1,
visible: '1',
status: '1'
};
await menuManagementPage.fillMenuForm(menuData);
});
await test.step('提交表单', async () => {
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
await test.step('验证菜单创建成功', async () => {
await menuManagementPage.reload();
const timestamp = Date.now();
const menuCreated = await menuManagementPage.containsText(`测试菜单_${timestamp}`);
expect(menuCreated).toBe(true);
});
});
test('MENU-003: 创建二级菜单', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('展开父级菜单', async () => {
await menuManagementPage.expandAll();
await page.waitForTimeout(1000);
});
await test.step('点击新增菜单按钮', async () => {
await menuManagementPage.clickCreateMenu();
});
await test.step('填写二级菜单信息', async () => {
const timestamp = Date.now();
const menuData = {
menuName: `测试子菜单_${timestamp}`,
menuType: '菜单',
path: `/test-submenu-${timestamp}`,
component: `TestSubmenu${timestamp}`,
permission: `system:test:submenu:${timestamp}`,
sort: 1,
visible: '1',
status: '1'
};
await menuManagementPage.fillMenuForm(menuData);
});
await test.step('提交表单', async () => {
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('MENU-004: 编辑菜单', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('编辑现有菜单', async () => {
const menuName = '系统管理';
await menuManagementPage.editMenu(menuName);
await page.waitForTimeout(500);
});
await test.step('修改菜单信息', async () => {
const timestamp = Date.now();
const updateData = {
menuName: `系统管理_更新_${timestamp}`,
sort: 2
};
await menuManagementPage.fillMenuForm(updateData);
});
await test.step('提交修改', async () => {
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('MENU-005: 删除菜单', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('创建测试菜单', async () => {
await menuManagementPage.clickCreateMenu();
const timestamp = Date.now();
const menuData = {
menuName: `待删除菜单_${timestamp}`,
menuType: '目录',
path: `/delete-test-${timestamp}`,
sort: 99,
visible: '1',
status: '1'
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
await page.waitForTimeout(1000);
});
await test.step('删除菜单', async () => {
const timestamp = Date.now();
await menuManagementPage.deleteMenu(`待删除菜单_${timestamp}`);
await menuManagementPage.confirmDelete();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
await test.step('验证菜单已删除', async () => {
await menuManagementPage.reload();
const timestamp = Date.now();
const menuDeleted = await menuManagementPage.containsText(`待删除菜单_${timestamp}`);
expect(menuDeleted).toBe(false);
});
});
test('MENU-006: 搜索菜单', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('搜索菜单', async () => {
await menuManagementPage.search('系统管理');
await page.waitForTimeout(1000);
});
await test.step('验证搜索结果', async () => {
const searchResult = await menuManagementPage.containsText('系统管理');
expect(searchResult).toBe(true);
});
await test.step('清除搜索', async () => {
await menuManagementPage.search('');
await page.waitForTimeout(1000);
const menuCount = await menuManagementPage.getMenuCount();
expect(menuCount).toBeGreaterThan(0);
});
});
test('MENU-007: 菜单树展开和折叠', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('展开所有菜单', async () => {
await menuManagementPage.expandAll();
await page.waitForTimeout(1000);
await expect(menuManagementPage.treeContainer).toBeVisible();
});
await test.step('折叠所有菜单', async () => {
await menuManagementPage.collapseAll();
await page.waitForTimeout(1000);
await expect(menuManagementPage.treeContainer).toBeVisible();
});
});
test('MENU-008: 菜单排序功能', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('创建多个菜单测试排序', async () => {
for (let i = 1; i <= 3; i++) {
await menuManagementPage.clickCreateMenu();
const timestamp = Date.now();
const menuData = {
menuName: `排序测试菜单_${i}_${timestamp}`,
menuType: '目录',
path: `/sort-test-${i}-${timestamp}`,
sort: i,
visible: '1',
status: '1'
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
await page.waitForTimeout(500);
}
});
await test.step('验证菜单按排序号显示', async () => {
await menuManagementPage.reload();
await page.waitForTimeout(1000);
const menuCount = await menuManagementPage.getMenuCount();
expect(menuCount).toBeGreaterThan(0);
});
});
test('MENU-009: 菜单可见性控制', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('创建可见菜单', async () => {
await menuManagementPage.clickCreateMenu();
const timestamp = Date.now();
const menuData = {
menuName: `可见菜单_${timestamp}`,
menuType: '菜单',
path: `/visible-menu-${timestamp}`,
sort: 1,
visible: '1',
status: '1'
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
await test.step('创建隐藏菜单', async () => {
await menuManagementPage.clickCreateMenu();
const timestamp = Date.now();
const menuData = {
menuName: `隐藏菜单_${timestamp}`,
menuType: '菜单',
path: `/hidden-menu-${timestamp}`,
sort: 2,
visible: '0',
status: '1'
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('MENU-010: 菜单状态管理', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('创建启用状态的菜单', async () => {
await menuManagementPage.clickCreateMenu();
const timestamp = Date.now();
const menuData = {
menuName: `启用菜单_${timestamp}`,
menuType: '菜单',
path: `/enabled-menu-${timestamp}`,
sort: 1,
visible: '1',
status: '1'
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
await test.step('创建禁用状态的菜单', async () => {
await menuManagementPage.clickCreateMenu();
const timestamp = Date.now();
const menuData = {
menuName: `禁用菜单_${timestamp}`,
menuType: '菜单',
path: `/disabled-menu-${timestamp}`,
sort: 2,
visible: '1',
status: '0'
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('MENU-011: 菜单权限标识', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('创建带权限标识的菜单', async () => {
await menuManagementPage.clickCreateMenu();
const timestamp = Date.now();
const menuData = {
menuName: `权限菜单_${timestamp}`,
menuType: '菜单',
path: `/permission-menu-${timestamp}`,
component: `PermissionMenu${timestamp}`,
permission: `system:permission:menu:${timestamp}`,
sort: 1,
visible: '1',
status: '1'
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('MENU-012: 菜单组件路径配置', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('创建带组件路径的菜单', async () => {
await menuManagementPage.clickCreateMenu();
const timestamp = Date.now();
const menuData = {
menuName: `组件菜单_${timestamp}`,
menuType: '菜单',
path: `/component-menu-${timestamp}`,
component: `system/ComponentMenu${timestamp}`,
sort: 1,
visible: '1',
status: '1'
};
await menuManagementPage.fillMenuForm(menuData);
await menuManagementPage.submitForm();
await expect(menuManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
});
});
test('MENU-013: 菜单响应式布局', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('验证桌面端布局', async () => {
await page.setViewportSize({ width: 1280, height: 720 });
await expect(menuManagementPage.table).toBeVisible();
await expect(menuManagementPage.createMenuButton).toBeVisible();
});
await test.step('验证平板端布局', async () => {
await page.setViewportSize({ width: 768, height: 1024 });
await expect(menuManagementPage.table).toBeVisible();
await expect(menuManagementPage.createMenuButton).toBeVisible();
});
await test.step('验证移动端布局', async () => {
await page.setViewportSize({ width: 375, height: 667 });
await expect(menuManagementPage.table).toBeVisible();
});
});
test('MENU-014: 菜单数据验证', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => {
await menuManagementPage.goto();
});
await test.step('验证菜单数据完整性', async () => {
const menuCount = await menuManagementPage.getMenuCount();
expect(menuCount).toBeGreaterThan(0);
});
await test.step('验证表格包含必要列', async () => {
await expect(menuManagementPage.table).toContainText('菜单名称');
await expect(menuManagementPage.table).toContainText('类型');
await expect(menuManagementPage.table).toContainText('路径');
await expect(menuManagementPage.table).toContainText('排序');
});
});
});
-195
View File
@@ -1,195 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { NotificationPage } from './pages/NotificationPage';
test.describe('通知公告E2E测试', () => {
let loginPage: LoginPage;
let noticePage: NotificationPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
noticePage = new NotificationPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
test.afterEach(async ({ page }) => {
await loginPage.logout();
});
test('通知公告页面导航', async ({ page }) => {
await test.step('导航到通知公告页面', async () => {
await noticePage.goto();
await expect(page).toHaveURL(/.*notice/);
});
await test.step('验证页面元素可见', async () => {
await expect(noticePage.table).toBeVisible();
await expect(noticePage.addButton).toBeVisible();
await expect(noticePage.searchInput).toBeVisible();
});
});
test('创建通知公告', async ({ page }) => {
await test.step('导航到通知公告页面', async () => {
await noticePage.goto();
});
await test.step('创建新通知公告', async () => {
const title = `测试通知_${Date.now()}`;
const content = `这是一条测试通知内容_${Date.now()}`;
await noticePage.addNotification(title, content);
await noticePage.verifyTableContains(title);
});
});
test('编辑通知公告', async ({ page }) => {
await test.step('导航到通知公告页面', async () => {
await noticePage.goto();
});
await test.step('编辑现有通知公告', async () => {
const title = '系统维护通知';
const newContent = `系统将于今晚进行维护,请提前保存工作_${Date.now()}`;
await noticePage.editNotification(title, newContent);
await noticePage.verifyTableContains(title);
});
});
test('删除通知公告', async ({ page }) => {
await test.step('导航到通知公告页面', async () => {
await noticePage.goto();
});
await test.step('删除通知公告', async () => {
const title = `测试通知_${Date.now()}`;
const content = `这是一条测试通知内容_${Date.now()}`;
await noticePage.addNotification(title, content);
await noticePage.verifyTableContains(title);
await noticePage.deleteNotification(title);
await noticePage.verifyTableNotContains(title);
});
});
test('搜索通知公告', async ({ page }) => {
await test.step('导航到通知公告页面', async () => {
await noticePage.goto();
});
await test.step('搜索通知公告', async () => {
const title = '系统维护通知';
await noticePage.searchNotification(title);
await noticePage.verifyTableContains(title);
});
await test.step('清除搜索', async () => {
await noticePage.clearSearch();
const rowCount = await noticePage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('通知公告分页功能', async ({ page }) => {
await test.step('导航到通知公告页面', async () => {
await noticePage.goto();
});
await test.step('验证表格数据加载', async () => {
const rowCount = await noticePage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('通知公告响应式布局', async ({ page }) => {
await test.step('导航到通知公告页面', async () => {
await noticePage.goto();
});
await test.step('验证桌面端布局', async () => {
await page.setViewportSize({ width: 1280, height: 720 });
await expect(noticePage.table).toBeVisible();
await expect(noticePage.addButton).toBeVisible();
});
await test.step('验证平板端布局', async () => {
await page.setViewportSize({ width: 768, height: 1024 });
await expect(noticePage.table).toBeVisible();
await expect(noticePage.addButton).toBeVisible();
});
await test.step('验证移动端布局', async () => {
await page.setViewportSize({ width: 375, height: 667 });
await expect(noticePage.table).toBeVisible();
});
});
test('通知公告权限验证', async ({ page }) => {
await test.step('导航到通知公告页面', async () => {
await noticePage.goto();
});
await test.step('验证添加按钮可见性', async () => {
await expect(noticePage.addButton).toBeVisible();
});
await test.step('验证编辑和删除按钮可见性', async () => {
const rows = await noticePage.table.locator('.el-table__row').count();
if (rows > 0) {
await expect(noticePage.table).toBeVisible();
}
});
});
test('通知公告状态管理', async ({ page }) => {
await test.step('导航到通知公告页面', async () => {
await noticePage.goto();
});
await test.step('创建已发布通知', async () => {
const title = `已发布通知_${Date.now()}`;
const content = `这是一条已发布的通知_${Date.now()}`;
await noticePage.addNotification(title, content, '1', '0');
await noticePage.verifyTableContains(title);
});
await test.step('创建草稿通知', async () => {
const title = `草稿通知_${Date.now()}`;
const content = `这是一条草稿通知_${Date.now()}`;
await noticePage.addNotification(title, content, '1', '1');
await noticePage.verifyTableContains(title);
});
});
test('通知公告内容验证', async ({ page }) => {
await test.step('导航到通知公告页面', async () => {
await noticePage.goto();
});
await test.step('验证通知标题长度限制', async () => {
const longTitle = '这是一个非常非常长的通知标题,用于测试系统对长标题的处理能力,确保系统能够正确显示和存储长标题';
const content = '测试内容';
await noticePage.addNotification(longTitle, content);
await noticePage.verifyTableContains(longTitle.substring(0, 50));
});
await test.step('验证通知内容格式', async () => {
const title = `格式测试通知_${Date.now()}`;
const content = '支持富文本格式:<b>粗体</b>、<i>斜体</i>、<u>下划线</u>';
await noticePage.addNotification(title, content);
await noticePage.verifyTableContains(title);
});
});
});
@@ -1,192 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { OperationLogPage } from './pages/OperationLogPage';
test.describe('操作日志E2E测试', () => {
let loginPage: LoginPage;
let operationLogPage: OperationLogPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
operationLogPage = new OperationLogPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
test.afterEach(async ({ page }) => {
await loginPage.logout();
});
test('操作日志页面导航', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
await expect(page).toHaveURL(/.*oplog/);
});
await test.step('验证页面元素可见', async () => {
await expect(operationLogPage.table).toBeVisible();
await expect(operationLogPage.searchInput).toBeVisible();
await expect(operationLogPage.exportButton).toBeVisible();
});
});
test('搜索操作日志', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
});
await test.step('搜索操作日志', async () => {
const keyword = 'admin';
await operationLogPage.searchByKeyword(keyword);
await operationLogPage.verifyTableContains(keyword);
});
await test.step('清除搜索', async () => {
await operationLogPage.clearSearch();
const rowCount = await operationLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('操作日志分页功能', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
});
await test.step('验证表格数据加载', async () => {
const rowCount = await operationLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('操作日志响应式布局', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
});
await test.step('验证桌面端布局', async () => {
await page.setViewportSize({ width: 1280, height: 720 });
await expect(operationLogPage.table).toBeVisible();
await expect(operationLogPage.exportButton).toBeVisible();
});
await test.step('验证平板端布局', async () => {
await page.setViewportSize({ width: 768, height: 1024 });
await expect(operationLogPage.table).toBeVisible();
await expect(operationLogPage.exportButton).toBeVisible();
});
await test.step('验证移动端布局', async () => {
await page.setViewportSize({ width: 375, height: 667 });
await expect(operationLogPage.table).toBeVisible();
});
});
test('操作日志数据验证', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
});
await test.step('验证日志数据完整性', async () => {
const rowCount = await operationLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
await test.step('验证日志字段显示', async () => {
await expect(operationLogPage.table).toBeVisible();
});
});
test('操作日志搜索功能', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
});
await test.step('按操作人搜索', async () => {
const operator = 'admin';
await operationLogPage.searchByKeyword(operator);
await operationLogPage.verifyTableContains(operator);
});
await test.step('按操作模块搜索', async () => {
const module = '用户管理';
await operationLogPage.searchByKeyword(module);
});
await test.step('清除搜索结果', async () => {
await operationLogPage.clearSearch();
const rowCount = await operationLogPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('操作日志导出功能', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
});
await test.step('导出操作日志', async () => {
const downloadPromise = page.waitForEvent('download');
await operationLogPage.exportData();
const download = await downloadPromise;
expect(download).toBeDefined();
});
});
test('操作日志时间范围验证', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
});
await test.step('验证日志时间戳显示', async () => {
const rowCount = await operationLogPage.getTableRowCount();
if (rowCount > 0) {
await expect(operationLogPage.table).toBeVisible();
}
});
});
test('操作日志权限验证', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
});
await test.step('验证导出按钮可见性', async () => {
await expect(operationLogPage.exportButton).toBeVisible();
});
await test.step('验证搜索功能可用', async () => {
await expect(operationLogPage.searchInput).toBeVisible();
await expect(operationLogPage.searchButton).toBeVisible();
});
});
test('操作日志详情查看', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
});
await test.step('验证日志详情显示', async () => {
const rowCount = await operationLogPage.getTableRowCount();
if (rowCount > 0) {
await expect(operationLogPage.table).toBeVisible();
}
});
});
test('操作日志排序功能', async ({ page }) => {
await test.step('导航到操作日志页面', async () => {
await operationLogPage.goto();
});
await test.step('验证表格排序功能', async () => {
const rowCount = await operationLogPage.getTableRowCount();
if (rowCount > 0) {
await expect(operationLogPage.table).toBeVisible();
}
});
});
});
@@ -3,24 +3,24 @@ import { Page, Locator, expect } from '@playwright/test';
export class DictionaryManagementPage {
readonly page: Page;
readonly table: Locator;
readonly createDictTypeButton: Locator;
readonly createDictDataButton: Locator;
readonly searchInput: Locator;
readonly searchButton: Locator;
readonly successMessage: Locator;
readonly dictTypeTable: Locator;
readonly dictDataTable: Locator;
readonly createDictButton: Locator;
readonly saveButton: Locator;
readonly dialog: Locator;
readonly dictNameInput: Locator;
readonly dictTypeInput: Locator;
readonly statusSelect: Locator;
readonly remarkInput: Locator;
constructor(page: Page) {
this.page = page;
this.table = page.locator('.el-table').or(page.locator('.dict-table'));
this.createDictTypeButton = page.getByRole('button', { name: '新增字典类型' }).or(page.locator('button:has-text("新增字典类型")'));
this.createDictDataButton = page.getByRole('button', { name: '新增字典数据' }).or(page.locator('button:has-text("新增字典数据")'));
this.searchInput = page.locator('input[placeholder*="搜索"]').or(page.locator('input[name*="keyword"]'));
this.searchButton = page.getByRole('button', { name: '搜索' }).or(page.locator('button:has-text("搜索")'));
this.successMessage = page.locator('.el-message--success').or(page.locator('.success-message'));
this.dictTypeTable = page.locator('.dict-type-table').or(page.locator('.el-table').first());
this.dictDataTable = page.locator('.dict-data-table').or(page.locator('.el-table').nth(1));
this.table = page.locator('.el-table');
this.createDictButton = page.getByRole('button', { name: '新增字典' });
this.saveButton = page.getByRole('button', { name: '确定' });
this.dialog = page.locator('.el-dialog');
this.dictNameInput = page.locator('.el-dialog').getByRole('textbox', { name: '字典名称' });
this.dictTypeInput = page.locator('.el-dialog').getByRole('textbox', { name: '字典类型' });
this.statusSelect = page.locator('.el-dialog').getByRole('combobox', { name: '状态' });
this.remarkInput = page.locator('.el-dialog').getByRole('textbox', { name: '备注' });
}
async goto() {
@@ -40,156 +40,57 @@ export class DictionaryManagementPage {
}
}
async clickCreateDictType() {
await this.createDictTypeButton.click();
async createDict(dictName: string, dictType: string, status: string = '0', remark?: string) {
await this.createDictButton.click();
await this.page.waitForTimeout(500);
await this.dictNameInput.fill(dictName);
await this.dictTypeInput.fill(dictType);
if (status) {
await this.statusSelect.click();
await this.page.waitForTimeout(300);
await this.page.getByRole('option', { name: status === '0' ? '正常' : '停用' }).click();
}
if (remark) {
await this.remarkInput.fill(remark);
}
await this.saveButton.click();
await this.page.waitForLoadState('networkidle');
}
async clickCreateDictData() {
await this.createDictDataButton.click();
async editDict(dictName: string, newDictName: string) {
const row = this.table.locator('tr').filter({ hasText: dictName }).first();
const editBtn = row.getByRole('button', { name: '编辑' });
await editBtn.click();
await this.page.waitForTimeout(500);
await this.dictNameInput.clear();
await this.dictNameInput.fill(newDictName);
await this.saveButton.click();
await this.page.waitForLoadState('networkidle');
}
async fillDictTypeForm(dictTypeData: {
dictName: string;
dictType: string;
status?: string;
remark?: string;
}) {
const dialog = this.page.locator('.el-dialog');
async deleteDict(dictName: string) {
const row = this.table.locator('tr').filter({ hasText: dictName }).first();
const deleteBtn = row.getByRole('button', { name: '删除' });
await deleteBtn.click();
await this.page.waitForTimeout(500);
await dialog.locator('input').first().fill(dictTypeData.dictName);
await dialog.locator('input').nth(1).fill(dictTypeData.dictType);
if (dictTypeData.status) {
const statusRadio = dialog.locator(`input[value="${dictTypeData.status}"]`);
if (await statusRadio.count() > 0) {
await statusRadio.check();
}
}
if (dictTypeData.remark) {
const remarkInput = dialog.locator('textarea');
if (await remarkInput.count() > 0) {
await remarkInput.fill(dictTypeData.remark);
}
}
const confirmBtn = this.page.locator('.el-message-box').getByRole('button', { name: '确定' });
await confirmBtn.click();
await this.page.waitForLoadState('networkidle');
}
async fillDictDataForm(dictData: {
dictLabel: string;
dictValue: string;
dictType?: string;
cssClass?: string;
listClass?: string;
isDefault?: string;
status?: string;
sort?: number;
}) {
const dialog = this.page.locator('.el-dialog');
await dialog.locator('input').first().fill(dictData.dictLabel);
await dialog.locator('input').nth(1).fill(dictData.dictValue);
if (dictData.dictType) {
const dictTypeSelect = dialog.locator('.el-select');
if (await dictTypeSelect.count() > 0) {
await dictTypeSelect.click();
await this.page.waitForTimeout(300);
await this.page.getByRole('option', { name: dictData.dictType }).click();
}
}
if (dictData.cssClass) {
const cssClassInput = dialog.locator('input[placeholder*="CSS"]');
if (await cssClassInput.count() > 0) {
await cssClassInput.fill(dictData.cssClass);
}
}
if (dictData.listClass) {
const listClassInput = dialog.locator('input[placeholder*="列表"]');
if (await listClassInput.count() > 0) {
await listClassInput.fill(dictData.listClass);
}
}
if (dictData.isDefault) {
const defaultRadio = dialog.locator(`input[value="${dictData.isDefault}"]`);
if (await defaultRadio.count() > 0) {
await defaultRadio.check();
}
}
if (dictData.status) {
const statusRadio = dialog.locator(`input[value="${dictData.status}"]`);
if (await statusRadio.count() > 0) {
await statusRadio.check();
}
}
if (dictData.sort !== undefined) {
const sortInput = dialog.locator('input[type="number"]');
if (await sortInput.count() > 0) {
await sortInput.fill(String(dictData.sort));
}
}
}
async submitForm() {
await this.page.getByRole('button', { name: '确定' }).or(this.page.locator('button:has-text("确定")')).click();
}
async editDictType(dictName: string) {
const dictTypeRow = this.dictTypeTable.locator('tbody tr').filter({ hasText: dictName });
await dictTypeRow.getByRole('button', { name: '编辑' }).or(this.page.locator('.edit-button')).click();
}
async editDictData(dictLabel: string) {
const dictDataRow = this.dictDataTable.locator('tbody tr').filter({ hasText: dictLabel });
await dictDataRow.getByRole('button', { name: '编辑' }).or(this.page.locator('.edit-button')).click();
}
async deleteDictType(dictName: string) {
const dictTypeRow = this.dictTypeTable.locator('tbody tr').filter({ hasText: dictName });
await dictTypeRow.getByRole('button', { name: '删除' }).or(this.page.locator('.delete-button')).click();
}
async deleteDictData(dictLabel: string) {
const dictDataRow = this.dictDataTable.locator('tbody tr').filter({ hasText: dictLabel });
await dictDataRow.getByRole('button', { name: '删除' }).or(this.page.locator('.delete-button')).click();
}
async confirmDelete() {
await this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.confirm-dialog .confirm-button')).click();
}
async search(keyword: string) {
await this.searchInput.fill(keyword);
await this.searchButton.click();
async getDictCount() {
const rows = await this.table.locator('.el-table__row').count();
return rows;
}
async containsText(text: string): Promise<boolean> {
return await this.table.getByText(text).count() > 0;
}
async isSuccessMessageVisible(): Promise<boolean> {
try {
return await this.successMessage.isVisible({ timeout: 3000 });
} catch {
return false;
}
}
async getDictTypeCount(): Promise<number> {
return await this.dictTypeTable.locator('tbody tr').count();
}
async getDictDataCount(): Promise<number> {
return await this.dictDataTable.locator('tbody tr').count();
}
async reload() {
await this.page.reload();
}
}
@@ -4,89 +4,85 @@ export class NotificationPage {
readonly page: Page;
readonly table;
readonly addButton;
readonly editButton;
readonly deleteButton;
readonly saveButton;
readonly cancelButton;
readonly searchInput;
readonly searchButton;
readonly dialog;
readonly titleInput;
readonly contentInput;
readonly typeSelect;
readonly noticeTypeSelect;
readonly statusSelect;
constructor(page: Page) {
this.page = page;
this.table = page.locator('.el-table');
this.addButton = page.getByRole('button', { name: '新增' });
this.editButton = page.getByRole('button', { name: '修改' });
this.deleteButton = page.getByRole('button', { name: '删除' });
this.addButton = page.getByRole('button', { name: '新增公告' });
this.saveButton = page.getByRole('button', { name: '确定' });
this.cancelButton = page.getByRole('button', { name: '取消' });
this.searchInput = page.getByPlaceholder('搜索通知标题');
this.searchButton = page.getByRole('button', { name: '搜索' });
this.titleInput = page.getByPlaceholder('请输入通知标题');
this.contentInput = page.getByPlaceholder('请输入通知内容');
this.typeSelect = page.locator('.el-select');
this.statusSelect = page.locator('.el-select');
this.dialog = page.locator('.el-dialog');
this.titleInput = page.locator('.el-dialog').getByRole('textbox', { name: '公告标题' });
this.contentInput = page.locator('.el-dialog').getByRole('textbox', { name: '公告内容' });
this.noticeTypeSelect = page.locator('.el-dialog').getByRole('combobox', { name: '公告类型' });
this.statusSelect = page.locator('.el-dialog').getByRole('combobox', { name: '状态' });
}
async goto() {
await this.page.goto('/system/notice');
await this.page.waitForLoadState('networkidle');
try {
console.log('导航到通知管理页面...');
await this.page.goto('/notice');
await this.page.waitForLoadState('networkidle');
await this.table.waitFor({ state: 'visible', timeout: 10000 });
await expect(this.page).toHaveURL(/.*notice/);
console.log('通知管理页面加载完成');
} catch (error) {
await this.page.screenshot({ path: `test-results/notification-error-${Date.now()}.png` });
console.error('导航到通知管理页面失败:', error);
throw new Error(`导航到通知管理页面失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
async addNotification(title: string, content: string, type: string = '1', status: string = '0') {
async addNotification(title: string, content: string) {
await this.addButton.click();
await this.page.waitForTimeout(500);
await this.titleInput.fill(title);
await this.contentInput.fill(content);
await this.saveButton.click();
await this.page.waitForLoadState('networkidle');
}
async editNotification(title: string, newContent: string) {
const row = this.table.locator('tr').filter({ hasText: title }).first();
await row.locator('.el-button--primary').click();
const editBtn = row.getByRole('button', { name: '编辑' });
await editBtn.click();
await this.page.waitForTimeout(500);
await this.contentInput.clear();
await this.contentInput.fill(newContent);
await this.saveButton.click();
await this.page.waitForLoadState('networkidle');
}
async deleteNotification(title: string) {
const row = this.table.locator('tr').filter({ hasText: title }).first();
await row.locator('.el-button--danger').click();
await this.saveButton.click();
const deleteBtn = row.getByRole('button', { name: '删除' });
await deleteBtn.click();
await this.page.waitForTimeout(500);
const confirmBtn = this.page.locator('.el-message-box').getByRole('button', { name: '确定' });
await confirmBtn.click();
await this.page.waitForLoadState('networkidle');
}
async searchNotification(keyword: string) {
await this.searchInput.fill(keyword);
await this.searchButton.click();
await this.page.waitForLoadState('networkidle');
}
async clearSearch() {
await this.searchInput.clear();
await this.searchButton.click();
await this.page.waitForLoadState('networkidle');
}
async verifyTableContains(text: string) {
await expect(this.table).toContainText(text);
}
async verifyTableNotContains(text: string) {
await expect(this.table).not.toContainText(text);
}
async getTableRowCount() {
const rows = await this.table.locator('.el-table__row').count();
return rows;
}
}
async verifyTableContains(text: string) {
await expect(this.table).toContainText(text);
}
}
@@ -4,31 +4,23 @@ export class SystemConfigPage {
readonly page: Page;
readonly table;
readonly addButton;
readonly editButton;
readonly deleteButton;
readonly saveButton;
readonly cancelButton;
readonly searchInput;
readonly searchButton;
readonly dialog;
readonly configNameInput;
readonly configKeyInput;
readonly configValueInput;
readonly configTypeSelect;
constructor(page: Page) {
this.page = page;
this.table = page.locator('.el-table');
this.addButton = page.getByRole('button', { name: '新增配置' });
this.editButton = page.getByRole('button', { name: '编辑' });
this.deleteButton = page.getByRole('button', { name: '删除' });
this.saveButton = page.getByRole('button', { name: '确定' });
this.cancelButton = page.getByRole('button', { name: '取消' });
this.searchInput = page.getByPlaceholder('搜索配置名称');
this.searchButton = page.getByRole('button', { name: '搜索' });
this.configNameInput = page.getByPlaceholder('请输入配置名称');
this.configKeyInput = page.getByPlaceholder('请输入配置键名');
this.configValueInput = page.getByPlaceholder('请输入配置键值');
this.configTypeSelect = page.locator('.el-select');
this.dialog = page.locator('.el-dialog');
this.configNameInput = page.locator('.el-dialog').getByRole('textbox', { name: '参数名称' });
this.configKeyInput = page.locator('.el-dialog').getByRole('textbox', { name: '参数键名' });
this.configValueInput = page.locator('.el-dialog').getByRole('textbox', { name: '参数值' });
}
async goto() {
@@ -48,8 +40,9 @@ export class SystemConfigPage {
}
}
async addConfig(configName: string, configKey: string, configValue: string, configType: string = 'Y') {
async addConfig(configName: string, configKey: string, configValue: string) {
await this.addButton.click();
await this.page.waitForTimeout(500);
await this.configNameInput.fill(configName);
await this.configKeyInput.fill(configKey);
@@ -61,7 +54,9 @@ export class SystemConfigPage {
async editConfig(configKey: string, newValue: string) {
const row = this.table.locator('tr').filter({ hasText: configKey }).first();
await row.locator('.el-button--primary').click();
const editBtn = row.getByRole('button', { name: '编辑' });
await editBtn.click();
await this.page.waitForTimeout(500);
await this.configValueInput.clear();
await this.configValueInput.fill(newValue);
@@ -72,34 +67,21 @@ export class SystemConfigPage {
async deleteConfig(configKey: string) {
const row = this.table.locator('tr').filter({ hasText: configKey }).first();
await row.locator('.el-button--danger').click();
const deleteBtn = row.getByRole('button', { name: '删除' });
await deleteBtn.click();
await this.page.waitForTimeout(500);
await this.saveButton.click();
const confirmBtn = this.page.locator('.el-message-box').getByRole('button', { name: '确定' });
await confirmBtn.click();
await this.page.waitForLoadState('networkidle');
}
async searchConfig(keyword: string) {
await this.searchInput.fill(keyword);
await this.searchButton.click();
await this.page.waitForLoadState('networkidle');
}
async clearSearch() {
await this.searchInput.clear();
await this.searchButton.click();
await this.page.waitForLoadState('networkidle');
}
async verifyTableContains(text: string) {
await expect(this.table).toContainText(text);
}
async verifyTableNotContains(text: string) {
await expect(this.table).not.toContainText(text);
}
async getTableRowCount() {
const rows = await this.table.locator('.el-table__row').count();
return rows;
}
}
async verifyTableContains(text: string) {
await expect(this.table).toContainText(text);
}
}
@@ -1,368 +0,0 @@
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 { MenuManagementPage } from './pages/MenuManagementPage';
import { SystemConfigPage } from './pages/SystemConfigPage';
// 测试用户配置
const TEST_USERS = {
superAdmin: {
username: 'admin',
password: 'password',
role: '超级管理员'
},
systemAdmin: {
username: 'sysadmin',
password: 'SysAdmin123!',
role: '系统管理员'
},
regularUser: {
username: 'user',
password: 'User123!',
role: '普通用户'
},
guest: {
username: '',
password: '',
role: '访客'
}
};
// 权限验证测试套件
test.describe('系统配置功能权限验证测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let userManagementPage: UserManagementPage;
let roleManagementPage: RoleManagementPage;
let menuManagementPage: MenuManagementPage;
let systemConfigPage: SystemConfigPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
userManagementPage = new UserManagementPage(page);
roleManagementPage = new RoleManagementPage(page);
menuManagementPage = new MenuManagementPage(page);
systemConfigPage = new SystemConfigPage(page);
});
// 测试1: 超级管理员权限验证
test('PERM-001: 超级管理员完整权限验证', async ({ page }) => {
const user = TEST_USERS.superAdmin;
const testResults = [];
await test.step(`1. ${user.role}登录系统`, async () => {
await loginPage.goto();
await loginPage.login(user.username, user.password);
await expect(page).toHaveURL(/.*dashboard/);
testResults.push({ step: '登录系统', result: '通过', details: '成功登录到仪表板' });
});
await test.step('2. 验证用户管理权限', async () => {
await dashboardPage.navigateToUserManagement();
// 验证用户管理页面可访问
await expect(page.locator('.user-management-header')).toBeVisible();
// 验证创建用户权限
await userManagementPage.clickCreateUser();
await expect(page.locator('.user-form')).toBeVisible();
testResults.push({ step: '用户管理权限', result: '通过', details: '可访问用户管理页面和创建用户功能' });
});
await test.step('3. 验证角色管理权限', async () => {
await dashboardPage.navigateToRoleManagement();
// 验证角色管理页面可访问
await expect(page.locator('.role-management-header')).toBeVisible();
// 验证创建角色权限
await roleManagementPage.clickCreateRole();
await expect(page.locator('.role-form')).toBeVisible();
testResults.push({ step: '角色管理权限', result: '通过', details: '可访问角色管理页面和创建角色功能' });
});
await test.step('4. 验证菜单管理权限', async () => {
await dashboardPage.navigateToMenuManagement();
// 验证菜单管理页面可访问
await expect(page.locator('.menu-management-header')).toBeVisible();
// 验证创建菜单权限
await menuManagementPage.clickCreateMenu();
await expect(page.locator('.menu-form')).toBeVisible();
testResults.push({ step: '菜单管理权限', result: '通过', details: '可访问菜单管理页面和创建菜单功能' });
});
await test.step('5. 验证系统配置权限', async () => {
await dashboardPage.navigateToSystemConfig();
// 验证系统配置页面可访问
await expect(page.locator('.system-config-header')).toBeVisible();
// 验证配置修改权限
await systemConfigPage.clickEditConfig();
await expect(page.locator('.config-form')).toBeVisible();
testResults.push({ step: '系统配置权限', result: '通过', details: '可访问系统配置页面和修改配置功能' });
});
// 生成测试报告
console.log(`\n=== ${user.role}权限验证报告 ===`);
testResults.forEach(result => {
console.log(`[${result.result}] ${result.step}: ${result.details}`);
});
});
// 测试2: 系统管理员权限验证
test('PERM-002: 系统管理员权限验证', async ({ page }) => {
const user = TEST_USERS.systemAdmin;
const testResults = [];
await test.step(`1. ${user.role}登录系统`, async () => {
await loginPage.goto();
await loginPage.login(user.username, user.password);
await expect(page).toHaveURL(/.*dashboard/);
testResults.push({ step: '登录系统', result: '通过', details: '成功登录到仪表板' });
});
await test.step('2. 验证用户管理权限', async () => {
await dashboardPage.navigateToUserManagement();
// 验证用户管理页面可访问
await expect(page.locator('.user-management-header')).toBeVisible();
// 验证创建用户权限
await userManagementPage.clickCreateUser();
await expect(page.locator('.user-form')).toBeVisible();
testResults.push({ step: '用户管理权限', result: '通过', details: '可访问用户管理页面和创建用户功能' });
});
await test.step('3. 验证角色管理权限', async () => {
await dashboardPage.navigateToRoleManagement();
// 验证角色管理页面可访问
await expect(page.locator('.role-management-header')).toBeVisible();
// 验证创建角色权限
await roleManagementPage.clickCreateRole();
await expect(page.locator('.role-form')).toBeVisible();
testResults.push({ step: '角色管理权限', result: '通过', details: '可访问角色管理页面和创建角色功能' });
});
await test.step('4. 验证菜单管理权限', async () => {
await dashboardPage.navigateToMenuManagement();
// 验证菜单管理页面可访问
await expect(page.locator('.menu-management-header')).toBeVisible();
// 验证创建菜单权限
await menuManagementPage.clickCreateMenu();
await expect(page.locator('.menu-form')).toBeVisible();
testResults.push({ step: '菜单管理权限', result: '通过', details: '可访问菜单管理页面和创建菜单功能' });
});
await test.step('5. 验证系统配置权限限制', async () => {
await dashboardPage.navigateToSystemConfig();
// 验证系统配置页面可访问
await expect(page.locator('.system-config-header')).toBeVisible();
// 验证配置修改权限(可能受限)
try {
await systemConfigPage.clickEditConfig();
await expect(page.locator('.config-form')).toBeVisible();
testResults.push({ step: '系统配置权限', result: '通过', details: '可访问系统配置页面和修改配置功能' });
} catch (error) {
testResults.push({ step: '系统配置权限', result: '受限', details: '系统配置修改功能受限' });
}
});
// 生成测试报告
console.log(`\n=== ${user.role}权限验证报告 ===`);
testResults.forEach(result => {
console.log(`[${result.result}] ${step}: ${result.details}`);
});
});
// 测试3: 普通用户权限验证
test('PERM-003: 普通用户权限验证', async ({ page }) => {
const user = TEST_USERS.regularUser;
const testResults = [];
await test.step(`1. ${user.role}登录系统`, async () => {
await loginPage.goto();
await loginPage.login(user.username, user.password);
await expect(page).toHaveURL(/.*dashboard/);
testResults.push({ step: '登录系统', result: '通过', details: '成功登录到仪表板' });
});
await test.step('2. 验证用户管理权限限制', async () => {
try {
await dashboardPage.navigateToUserManagement();
// 如果能够访问,验证是否有限制
const hasAccess = await page.locator('.user-management-header').isVisible();
if (hasAccess) {
testResults.push({ step: '用户管理权限', result: '受限', details: '可访问但功能受限' });
} else {
testResults.push({ step: '用户管理权限', result: '拒绝', details: '无法访问用户管理页面' });
}
} catch (error) {
testResults.push({ step: '用户管理权限', result: '拒绝', details: '权限不足,无法访问' });
}
});
await test.step('3. 验证角色管理权限限制', async () => {
try {
await dashboardPage.navigateToRoleManagement();
const hasAccess = await page.locator('.role-management-header').isVisible();
if (hasAccess) {
testResults.push({ step: '角色管理权限', result: '受限', details: '可访问但功能受限' });
} else {
testResults.push({ step: '角色管理权限', result: '拒绝', details: '无法访问角色管理页面' });
}
} catch (error) {
testResults.push({ step: '角色管理权限', result: '拒绝', details: '权限不足,无法访问' });
}
});
await test.step('4. 验证菜单管理权限限制', async () => {
try {
await dashboardPage.navigateToMenuManagement();
const hasAccess = await page.locator('.menu-management-header').isVisible();
if (hasAccess) {
testResults.push({ step: '菜单管理权限', result: '受限', details: '可访问但功能受限' });
} else {
testResults.push({ step: '菜单管理权限', result: '拒绝', details: '无法访问菜单管理页面' });
}
} catch (error) {
testResults.push({ step: '菜单管理权限', result: '拒绝', details: '权限不足,无法访问' });
}
});
await test.step('5. 验证系统配置权限限制', async () => {
try {
await dashboardPage.navigateToSystemConfig();
const hasAccess = await page.locator('.system-config-header').isVisible();
if (hasAccess) {
testResults.push({ step: '系统配置权限', result: '受限', details: '可访问但功能受限' });
} else {
testResults.push({ step: '系统配置权限', result: '拒绝', details: '无法访问系统配置页面' });
}
} catch (error) {
testResults.push({ step: '系统配置权限', result: '拒绝', details: '权限不足,无法访问' });
}
});
// 生成测试报告
console.log(`\n=== ${user.role}权限验证报告 ===`);
testResults.forEach(result => {
console.log(`[${result.result}] ${result.step}: ${result.details}`);
});
});
// 测试4: 访客权限验证
test('PERM-004: 访客权限验证', async ({ page }) => {
const user = TEST_USERS.guest;
const testResults = [];
await test.step('1. 直接访问系统管理页面', async () => {
await page.goto('/user-management');
// 验证是否被重定向到登录页面
const currentUrl = page.url();
if (currentUrl.includes('/login')) {
testResults.push({ step: '用户管理页面访问', result: '拒绝', details: '被重定向到登录页面' });
} else {
testResults.push({ step: '用户管理页面访问', result: '异常', details: '未正确重定向' });
}
});
await test.step('2. 直接访问角色管理页面', async () => {
await page.goto('/role-management');
const currentUrl = page.url();
if (currentUrl.includes('/login')) {
testResults.push({ step: '角色管理页面访问', result: '拒绝', details: '被重定向到登录页面' });
} else {
testResults.push({ step: '角色管理页面访问', result: '异常', details: '未正确重定向' });
}
});
await test.step('3. 直接访问菜单管理页面', async () => {
await page.goto('/menu-management');
const currentUrl = page.url();
if (currentUrl.includes('/login')) {
testResults.push({ step: '菜单管理页面访问', result: '拒绝', details: '被重定向到登录页面' });
} else {
testResults.push({ step: '菜单管理页面访问', result: '异常', details: '未正确重定向' });
}
});
await test.step('4. 直接访问系统配置页面', async () => {
await page.goto('/system-config');
const currentUrl = page.url();
if (currentUrl.includes('/login')) {
testResults.push({ step: '系统配置页面访问', result: '拒绝', details: '被重定向到登录页面' });
} else {
testResults.push({ step: '系统配置页面访问', result: '异常', details: '未正确重定向' });
}
});
// 生成测试报告
console.log(`\n=== ${user.role}权限验证报告 ===`);
testResults.forEach(result => {
console.log(`[${result.result}] ${result.step}: ${result.details}`);
});
});
// 测试5: 权限边界测试
test('PERM-005: 权限边界测试', async ({ page }) => {
const testResults = [];
await test.step('1. 测试越权访问', async () => {
// 使用普通用户登录
await loginPage.goto();
await loginPage.login(TEST_USERS.regularUser.username, TEST_USERS.regularUser.password);
await expect(page).toHaveURL(/.*dashboard/);
// 尝试直接访问管理员功能URL
await page.goto('/user-management/create');
// 验证是否被阻止
const isBlocked = await page.locator('.access-denied, .permission-error').isVisible() ||
page.url().includes('/login') ||
page.url().includes('/dashboard');
if (isBlocked) {
testResults.push({ step: '越权访问测试', result: '通过', details: '系统正确阻止了越权访问' });
} else {
testResults.push({ step: '越权访问测试', result: '失败', details: '系统未正确阻止越权访问' });
}
});
await test.step('2. 测试API权限验证', async () => {
// 模拟API调用权限验证
const apiResponse = await page.request.get('/api/users');
if (apiResponse.status() === 401 || apiResponse.status() === 403) {
testResults.push({ step: 'API权限验证', result: '通过', details: 'API权限验证正常工作' });
} else {
testResults.push({ step: 'API权限验证', result: '警告', details: 'API权限验证可能需要加强' });
}
});
// 生成测试报告
console.log('\n=== 权限边界测试报告 ===');
testResults.forEach(result => {
console.log(`[${result.result}] ${result.step}: ${result.details}`);
});
});
});
@@ -1,256 +0,0 @@
# 基于角色的用户模拟测试套件
## 概述
本测试套件实现了基于角色的用户模拟测试,用于验证后端管理系统的权限边界和业务流程。
## 架构设计
### 核心组件
1. **角色定义系统** (`roles/`)
- `base.role.ts` - 角色定义基类
- `admin.role.ts` - 管理员角色
- `user.role.ts` - 普通用户角色
- `test.role.ts` - 测试用户角色
- `role-factory.ts` - 角色工厂
2. **共享工具** (`shared/`)
- `role-auth-manager.ts` - Token管理器
- `auth-helper.ts` - 认证辅助工具
- `test-data-manager.ts` - 测试数据管理器
- `permission-helper.ts` - 权限验证工具
3. **测试场景** (`scenarios/`)
- `authentication/` - 认证场景测试
- `user-management/` - 用户管理场景测试
## 快速开始
### 环境准备
1. 确保后端服务运行在 `http://localhost:8084`
2. 确保前端服务运行在 `http://localhost:3002`
3. 确保H2数据库已初始化测试数据
### 运行测试
```bash
# 运行所有单元测试
pnpm test
# 运行角色测试项目
pnpm exec playwright test --project=role-based-tests
# 运行特定测试文件
pnpm exec playwright test e2e/role-based-tests/scenarios/authentication/login-flow.spec.ts
# 运行特定角色的测试
pnpm exec playwright test --project=role-based-tests --grep "管理员"
```
## 角色配置
### 测试用户
所有测试用户统一使用密码:`Test@123`
| 用户名 | 角色 | 说明 |
|--------|------|------|
| admin | 超级管理员 | 拥有所有权限 |
| normaluser | 普通用户 | 只能访问个人信息 |
| e2e_test_user | 测试用户 | 用于E2E测试 |
### 权限定义
每个角色定义包含:
- `permissions` - 拥有的权限列表
- `cannotAccess` - 无法访问的路径
- `expectedBehaviors` - 预期行为(CRUD权限)
## 测试场景
### 认证场景
- 登录流程测试(6个测试用例)
- 管理员用户登录成功
- 普通用户登录成功
- 错误密码登录失败
- 空用户名登录失败
- 空密码登录失败
- Token注入登录
- 登出流程测试(4个测试用例)
- 用户登出成功
- 登出后无法访问受保护页面
- 登出后Token被清除
- 多角色登出测试
### 用户管理场景
- 管理员创建用户测试(5个测试用例)
- 管理员可以创建新用户
- 管理员可以编辑用户信息
- 管理员可以删除用户
- 创建用户时用户名重复验证
- 创建用户时邮箱格式验证
- 权限边界验证测试(11个测试用例)
- 管理员权限验证(5个)
- 普通用户权限验证(4个)
- 测试用户权限验证(2个)
- 跨角色权限对比测试
## 测试数据管理
### 自动清理
测试数据管理器会自动跟踪创建的测试数据,并在测试结束后清理:
```typescript
import { getTestDataManager } from '../shared/test-data-manager';
test.afterEach(async () => {
await getTestDataManager().cleanup('user');
});
```
### 手动创建测试数据
```typescript
const testDataManager = getTestDataManager();
const user = await testDataManager.createUser({
username: 'testuser',
password: 'Test@123',
email: 'test@example.com',
});
```
## 认证方式
### Token注入(推荐)
```typescript
import { createAuthenticatedPage } from '../shared/auth-helper';
test.beforeEach(async ({ page, context }) => {
await createAuthenticatedPage(page, context, 'admin');
});
```
### 真实登录
```typescript
import { AuthHelper } from '../shared/auth-helper';
const authHelper = new AuthHelper(page, context);
await authHelper.loginAsRole('admin', false); // false表示使用真实登录
```
## 权限验证
```typescript
import { createPermissionHelper } from '../shared/permission-helper';
const permissionHelper = createPermissionHelper(page);
// 验证可以访问
await permissionHelper.verifyCanAccess('/user-management');
// 验证无法访问
await permissionHelper.verifyCannotAccess('/role-management');
// 验证角色权限边界
const role = RoleFactory.getRole('admin');
await permissionHelper.verifyRolePermissions(role);
```
## 最佳实践
1. **使用Token注入**:提升测试执行效率
2. **遵循TDD原则**:先写测试,再实现功能
3. **测试数据隔离**:每个测试独立创建和清理数据
4. **权限边界验证**:确保每个角色的权限边界清晰
5. **跨浏览器测试**:在Chrome、Firefox、Safari上运行测试
## 故障排查
### 登录失败
1. 检查后端服务是否运行
2. 检查数据库是否初始化
3. 检查密码是否正确(应为 `Test@123`
### 权限验证失败
1. 检查角色定义是否正确
2. 检查后端权限配置
3. 检查前端路由守卫
### 测试数据清理失败
1. 检查数据库连接
2. 检查API权限
3. 手动清理测试数据
## CI/CD集成
### Jenkins Pipeline示例
```groovy
stage('Role-Based Tests') {
steps {
sh 'pnpm install'
sh 'pnpm exec playwright test --project=role-based-tests'
}
post {
always {
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'playwright-report',
reportFiles: 'index.html',
reportName: 'Playwright Report'
])
}
}
}
```
## 维护指南
### 添加新角色
1.`roles/` 目录创建新的角色定义文件
2.`role-factory.ts` 中注册新角色
3.`data-h2.sql` 中添加测试用户数据
4. 编写对应的测试用例
### 添加新测试场景
1.`scenarios/` 目录创建新的测试文件
2. 使用现有的工具类(认证、数据管理、权限验证)
3. 确保测试数据隔离和清理
4. 更新文档
## 统计信息
- **单元测试**172个测试用例
- **E2E测试**26个测试场景
- **角色定义**3个角色
- **测试覆盖率**:核心功能100%
## 更新日志
### v1.0.0 (2026-04-04)
- ✅ 实现角色定义系统
- ✅ 实现认证辅助工具
- ✅ 实现测试数据管理器
- ✅ 实现权限验证工具
- ✅ 实现认证场景测试
- ✅ 实现用户管理场景测试
- ✅ 统一H2数据库密码配置
- ✅ 配置Playwright测试项目
@@ -1,83 +0,0 @@
import { test, expect } from '@playwright/test';
import { RoleFactory } from '@/role-based-tests/roles/role-factory';
import { createAuthenticatedPage } from '@/role-based-tests/shared/auth-helper';
test.describe('登录流程测试', () => {
test('管理员用户登录成功', async ({ page, context }) => {
const role = RoleFactory.getRole('admin');
await page.goto('/login');
await page.fill('input[placeholder*="用户名"]', role.credentials.username);
await page.fill('input[placeholder*="密码"]', role.credentials.password);
await page.click('button:has-text("登录")');
await expect(page).toHaveURL(/\/(dashboard|\/)?/, { timeout: 10000 });
await page.waitForLoadState('networkidle');
});
test('普通用户登录成功', async ({ page, context }) => {
const role = RoleFactory.getRole('user');
await page.goto('/login');
await page.fill('input[placeholder*="用户名"]', role.credentials.username);
await page.fill('input[placeholder*="密码"]', role.credentials.password);
await page.click('button:has-text("登录")');
await expect(page).toHaveURL(/\/(dashboard|\/)?/, { timeout: 10000 });
});
test('错误密码登录失败', async ({ page }) => {
await page.goto('/login');
await page.fill('input[placeholder*="用户名"]', 'admin');
await page.fill('input[placeholder*="密码"]', 'wrongpassword');
await Promise.all([
page.waitForResponse(resp => resp.url().includes('/auth/login') && resp.status() === 401),
page.click('button:has-text("登录")')
]);
const errorMessage = page.locator('.el-message');
await expect(errorMessage).toBeVisible({ timeout: 10000 });
await expect(errorMessage).toContainText(/用户名或密码错误|登录失败/i);
await expect(page).toHaveURL(/\/login/);
});
test('空用户名登录失败', async ({ page }) => {
await page.goto('/login');
await page.fill('input[placeholder*="密码"]', 'Test@123');
await page.click('input[placeholder*="用户名"]');
await page.click('input[placeholder*="密码"]');
await page.click('button:has-text("登录")');
const validationMessage = page.locator('.el-form-item__error');
await expect(validationMessage).toBeVisible({ timeout: 5000 });
});
test('空密码登录失败', async ({ page }) => {
await page.goto('/login');
await page.fill('input[placeholder*="用户名"]', 'admin');
await page.click('input[placeholder*="密码"]');
await page.click('input[placeholder*="用户名"]');
await page.click('button:has-text("登录")');
const validationMessage = page.locator('.el-form-item__error');
await expect(validationMessage).toBeVisible({ timeout: 5000 });
});
test('Token注入登录', async ({ page, context }) => {
await createAuthenticatedPage(page, context, 'admin');
await page.goto('/dashboard');
await expect(page).toHaveURL(/\/dashboard/);
await page.waitForLoadState('networkidle');
});
});
@@ -1,80 +0,0 @@
import { test, expect } from '@playwright/test';
import { RoleFactory } from '@/role-based-tests/roles/role-factory';
import { AuthHelper } from '@/role-based-tests/shared/auth-helper';
test.describe('登出流程测试', () => {
let authHelper: AuthHelper;
test.beforeEach(async ({ page, context }) => {
authHelper = new AuthHelper(page, context);
await authHelper.loginAsRole('admin');
});
test('用户登出成功', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForSelector('.el-dropdown', { state: 'visible' });
await page.click('.el-dropdown .el-avatar');
await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 });
await page.click('.el-dropdown-menu-item:has-text("退出登录")');
await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
const loginButton = page.locator('button:has-text("登录")');
await expect(loginButton).toBeVisible();
});
test('登出后无法访问受保护页面', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForSelector('.el-dropdown', { state: 'visible' });
await page.click('.el-dropdown .el-avatar');
await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 });
await page.click('.el-dropdown-menu-item:has-text("退出登录")');
await expect(page).toHaveURL(/\/login/);
await page.goto('/users');
await expect(page).toHaveURL(/\/login/);
});
test('登出后Token被清除', async ({ page, context }) => {
await page.goto('/dashboard');
await page.waitForSelector('.el-dropdown', { state: 'visible' });
await page.click('.el-dropdown .el-avatar');
await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 });
await page.click('.el-dropdown-menu-item:has-text("退出登录")');
await expect(page).toHaveURL(/\/login/);
const cookies = await context.cookies();
const tokenCookie = cookies.find(c => c.name === 'token');
expect(tokenCookie).toBeUndefined();
const localStorageToken = await page.evaluate(() => {
return localStorage.getItem('token');
});
expect(localStorageToken).toBeNull();
});
test('多角色登出测试', async ({ page, context }) => {
const roles = ['admin', 'user', 'test'];
for (const roleName of roles) {
const helper = new AuthHelper(page, context);
await helper.clearAuth();
await helper.loginAsRole(roleName);
await page.goto('/dashboard');
await page.waitForSelector('.el-dropdown', { state: 'visible' });
await page.click('.el-dropdown .el-avatar');
await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 });
await page.click('.el-dropdown-menu-item:has-text("退出登录")');
await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
}
});
});
@@ -1,102 +0,0 @@
import { test, expect } from '@playwright/test';
import { RoleFactory } from '@/role-based-tests/roles/role-factory';
import { createAuthenticatedPage } from '@/role-based-tests/shared/auth-helper';
import { getTestDataManager } from '@/role-based-tests/shared/test-data-manager';
test.describe('管理员创建用户测试', () => {
test.beforeEach(async ({ page, context }) => {
await createAuthenticatedPage(page, context, 'admin');
getTestDataManager().setPage(page);
});
test.afterEach(async () => {
await getTestDataManager().cleanup('user');
});
test('管理员可以创建新用户', async ({ page }) => {
await page.goto('/users');
await page.click('button:has-text("新增")');
const timestamp = Date.now();
const userData = {
username: `testuser_${timestamp}`,
password: 'Test@123',
email: `testuser_${timestamp}@test.com`,
phone: '13800138000',
nickname: '测试用户',
};
await page.fill('input[placeholder*="用户名"]', userData.username);
await page.fill('input[placeholder*="密码"]', userData.password);
await page.fill('input[placeholder*="邮箱"]', userData.email);
await page.fill('input[placeholder*="手机号"]', userData.phone);
await page.fill('input[placeholder*="昵称"]', userData.nickname);
await page.click('button:has-text("确定")');
const successMessage = page.locator('text=/创建成功|操作成功/i');
await expect(successMessage).toBeVisible({ timeout: 10000 });
const createdUser = page.locator(`text=${userData.username}`);
await expect(createdUser).toBeVisible();
});
test('管理员可以编辑用户信息', async ({ page }) => {
await page.goto('/users');
const firstEditButton = page.locator('button:has-text("编辑")').first();
await firstEditButton.click();
const nicknameInput = page.locator('input[placeholder*="昵称"]');
await nicknameInput.fill('更新后的昵称');
await page.click('button:has-text("确定")');
const successMessage = page.locator('text=/更新成功|操作成功/i');
await expect(successMessage).toBeVisible({ timeout: 10000 });
});
test('管理员可以删除用户', async ({ page }) => {
await page.goto('/users');
const firstDeleteButton = page.locator('button:has-text("删除")').first();
await firstDeleteButton.click();
const confirmButton = page.locator('button:has-text("确定")');
await confirmButton.click();
const successMessage = page.locator('text=/删除成功|操作成功/i');
await expect(successMessage).toBeVisible({ timeout: 10000 });
});
test('创建用户时用户名重复验证', async ({ page }) => {
await page.goto('/users');
await page.click('button:has-text("新增")');
await page.fill('input[placeholder*="用户名"]', 'admin');
await page.fill('input[placeholder*="密码"]', 'Test@123');
await page.fill('input[placeholder*="邮箱"]', 'admin@test.com');
await page.click('button:has-text("确定")');
const errorMessage = page.locator('text=/用户名已存在|用户名重复/i');
await expect(errorMessage).toBeVisible({ timeout: 5000 });
});
test('创建用户时邮箱格式验证', async ({ page }) => {
await page.goto('/users');
await page.click('button:has-text("新增")');
await page.fill('input[placeholder*="用户名"]', 'testuser');
await page.fill('input[placeholder*="密码"]', 'Test@123');
await page.fill('input[placeholder*="邮箱"]', 'invalid-email');
await page.click('button:has-text("确定")');
const errorMessage = page.locator('text=/邮箱格式不正确|请输入正确的邮箱/i');
await expect(errorMessage).toBeVisible({ timeout: 5000 });
});
});
@@ -1,132 +0,0 @@
import { test, expect } from '@playwright/test';
import { RoleFactory } from '@/role-based-tests/roles/role-factory';
import { createAuthenticatedPage } from '@/role-based-tests/shared/auth-helper';
import { createPermissionHelper } from '@/role-based-tests/shared/permission-helper';
test.describe('权限边界验证测试', () => {
test.describe('管理员权限', () => {
test.beforeEach(async ({ page, context }) => {
await createAuthenticatedPage(page, context, 'admin');
});
test('管理员可以访问用户管理页面', async ({ page }) => {
const permissionHelper = createPermissionHelper(page);
const adminRole = RoleFactory.getRole('admin');
await permissionHelper.verifyCanAccess('/users');
});
test('管理员可以访问角色管理页面', async ({ page }) => {
const permissionHelper = createPermissionHelper(page);
await permissionHelper.verifyCanAccess('/roles');
});
test('管理员可以创建用户', async ({ page }) => {
await page.goto('/users');
const createButton = page.locator('button:has-text("新增用户")');
await expect(createButton).toBeVisible();
await expect(createButton).toBeEnabled();
});
test('管理员可以编辑用户', async ({ page }) => {
await page.goto('/users');
await page.waitForLoadState('networkidle');
const editButton = page.locator('button:has-text("编辑")').first();
await expect(editButton).toBeVisible({ timeout: 5000 });
});
test('管理员可以删除用户', async ({ page }) => {
await page.goto('/users');
await page.waitForLoadState('networkidle');
const deleteButton = page.locator('button:has-text("删除")').first();
await expect(deleteButton).toBeVisible({ timeout: 5000 });
});
});
test.describe('普通用户权限', () => {
test.beforeEach(async ({ page, context }) => {
await createAuthenticatedPage(page, context, 'user');
});
test('普通用户无法访问用户管理页面', async ({ page }) => {
const permissionHelper = createPermissionHelper(page);
const userRole = RoleFactory.getRole('user');
await permissionHelper.verifyCannotAccess('/users');
});
test('普通用户无法访问角色管理页面', async ({ page }) => {
const permissionHelper = createPermissionHelper(page);
await permissionHelper.verifyCannotAccess('/roles');
});
test('普通用户可以访问个人中心', async ({ page }) => {
await page.goto('/profile');
await expect(page).not.toHaveURL(/\/login/);
await expect(page).not.toHaveURL(/\/403/);
});
test('普通用户可以修改个人信息', async ({ page }) => {
await page.goto('/profile');
const editButton = page.locator('button:has-text("编辑")');
const count = await editButton.count();
if (count > 0) {
await expect(editButton.first()).toBeVisible();
}
});
});
test.describe('测试用户权限', () => {
test.beforeEach(async ({ page, context }) => {
await createAuthenticatedPage(page, context, 'test');
});
test('测试用户无法访问用户管理页面', async ({ page }) => {
const permissionHelper = createPermissionHelper(page);
await permissionHelper.verifyCannotAccess('/users');
});
test('测试用户可以访问测试页面', async ({ page }) => {
await page.goto('/test');
await expect(page).not.toHaveURL(/\/login/);
await expect(page).not.toHaveURL(/\/403/);
});
});
test.describe('跨角色权限对比', () => {
test('不同角色访问权限对比', async ({ page, context }) => {
const roles = ['admin', 'user', 'test'];
const protectedPaths = ['/users', '/roles', '/menus'];
for (const roleName of roles) {
const role = RoleFactory.getRole(roleName);
const helper = new (await import('../../shared/auth-helper')).AuthHelper(page, context);
await helper.clearAuth();
await helper.loginAsRole(roleName);
for (const path of protectedPaths) {
await page.goto(path);
const isForbidden = role.cannotAccess.includes(path);
const url = page.url();
if (isForbidden) {
expect(url.includes('/403') || url.includes('/login')).toBeTruthy();
} else {
expect(url.includes('/403')).toBeFalsy();
}
}
}
});
});
});
@@ -1,147 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { RoleManagementPage } from './pages/RoleManagementPage';
test.describe('角色权限管理 E2E 测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let roleManagementPage: RoleManagementPage;
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('查看角色列表', async ({ page }) => {
await page.goto('/roles');
await page.waitForLoadState('networkidle');
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
const roleCount = await page.locator('.el-table__body tr').count();
expect(roleCount).toBeGreaterThan(0);
});
test('角色管理页面导航', async ({ page }) => {
await test.step('1. 导航到角色管理页面', async () => {
await page.goto('/roles');
await page.waitForLoadState('networkidle');
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
});
await test.step('2. 验证页面标题', async () => {
const pageTitle = await page.title();
expect(pageTitle).toContain('Novalon 管理系统');
});
await test.step('3. 验证表格结构', async () => {
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
const headers = await page.locator('.el-table__header th').count();
expect(headers).toBeGreaterThan(0);
});
});
test('角色搜索功能', async ({ page }) => {
await page.goto('/roles');
await page.waitForLoadState('networkidle');
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
const searchInput = page.locator('input[placeholder*="搜索"]').or(page.locator('.search-input'));
if (await searchInput.count() > 0) {
await searchInput.fill('admin');
await page.waitForTimeout(1000);
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
}
});
test('角色详情查看', async ({ page }) => {
await page.goto('/roles');
await page.waitForLoadState('networkidle');
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
const firstRow = page.locator('.el-table__body tr').first();
await firstRow.click();
await page.waitForTimeout(1000);
const currentUrl = page.url();
expect(currentUrl).toContain('/roles');
});
test('角色管理页面刷新', async ({ page }) => {
await page.goto('/roles');
await page.waitForLoadState('networkidle');
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
await page.reload();
await page.waitForLoadState('networkidle');
const tableAfterReload = page.locator('.el-table').first();
await expect(tableAfterReload).toBeVisible();
});
test('角色权限验证', async ({ page }) => {
await test.step('1. 确认管理员已登录', async () => {
const isLoggedIn = await loginPage.isLoggedIn();
expect(isLoggedIn).toBe(true);
});
await test.step('2. 访问角色管理页面', async () => {
await page.goto('/roles');
await page.waitForLoadState('networkidle');
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
});
await test.step('3. 验证可以查看角色数据', async () => {
const roleCount = await page.locator('.el-table__body tr').count();
expect(roleCount).toBeGreaterThan(0);
});
await test.step('4. 验证可以访问其他管理页面', async () => {
await page.goto('/users');
await page.waitForLoadState('networkidle');
const userTable = page.locator('.el-table').first();
await expect(userTable).toBeVisible();
});
});
test('角色管理响应式布局', async ({ page }) => {
await page.goto('/roles');
await page.waitForLoadState('networkidle');
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
await page.setViewportSize({ width: 768, height: 1024 });
await page.waitForTimeout(1000);
const mobileTable = page.locator('.el-table').first();
await expect(mobileTable).toBeVisible();
await page.setViewportSize({ width: 1920, height: 1080 });
await page.waitForTimeout(1000);
const desktopTable = page.locator('.el-table').first();
await expect(desktopTable).toBeVisible();
});
});
-290
View File
@@ -1,290 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';
test.describe('E2E安全测试', () => {
test('SEC-001: XSS攻击防护测试', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const userManagementPage = new UserManagementPage(page);
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
});
await test.step('2. 导航到用户管理', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
});
await test.step('3. 测试XSS payload防护', async () => {
const xssPayloads = [
'<script>alert("XSS")</script>',
'<img src=x onerror=alert("XSS")>',
'<svg onload=alert("XSS")>',
'javascript:alert("XSS")',
'<body onload=alert("XSS")>'
];
for (const payload of xssPayloads) {
const timestamp = Date.now();
const userData = {
username: `xss_test_${timestamp}`,
nickname: payload,
email: `xss_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
await page.waitForTimeout(1000);
if (await userManagementPage.isSuccessMessageVisible()) {
await userManagementPage.clickEditButton(1);
await page.waitForTimeout(500);
const pageContent = await page.content();
expect(pageContent).not.toContain('<script>');
expect(pageContent).not.toContain('onerror=');
expect(pageContent).not.toContain('onload=');
expect(pageContent).not.toContain('javascript:');
}
}
});
});
test('SEC-002: SQL注入防护测试', async ({ page }) => {
const loginPage = new LoginPage(page);
await test.step('1. 测试登录SQL注入防护', async () => {
await loginPage.goto();
const sqlPayloads = [
"admin' OR '1'='1",
"admin' --",
"admin' #",
"admin'/*",
"admin' or 1=1--",
"admin' union select * from users--"
];
for (const payload of sqlPayloads) {
await loginPage.usernameInput.fill(payload);
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await page.waitForTimeout(2000);
const currentUrl = page.url();
expect(currentUrl).toContain('/login');
try {
const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).toBeTruthy();
} catch (error) {
expect(currentUrl).toContain('/login');
}
}
});
});
test('SEC-003: 输入验证测试', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const userManagementPage = new UserManagementPage(page);
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
});
await test.step('2. 导航到用户管理', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
});
await test.step('3. 测试必填字段验证', async () => {
await userManagementPage.submitForm();
await page.waitForTimeout(500);
try {
const usernameError = await page.locator('.el-form-item__error').filter({ hasText: /用户名/ }).isVisible({ timeout: 2000 });
const passwordError = await page.locator('.el-form-item__error').filter({ hasText: /密码/ }).isVisible({ timeout: 2000 });
expect(usernameError || passwordError).toBeTruthy();
} catch (error) {
console.log('验证错误消息未显示');
expect(true).toBeTruthy();
}
});
await test.step('4. 测试邮箱格式验证', async () => {
const invalidEmails = [
'invalid',
'@example.com',
'test@',
'test@.com',
'test @example.com'
];
for (const invalidEmail of invalidEmails) {
await userManagementPage.fillUserForm({
username: `test_${Date.now()}`,
email: invalidEmail,
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
});
await userManagementPage.submitForm();
await page.waitForTimeout(500);
try {
const emailError = await page.locator('.el-form-item__error').filter({ hasText: /邮箱/ }).isVisible({ timeout: 2000 });
expect(emailError).toBeTruthy();
} catch (error) {
console.log(`邮箱验证错误未显示: ${invalidEmail}`);
expect(true).toBeTruthy();
}
}
});
await test.step('5. 测试密码强度验证', async () => {
const weakPasswords = [
'123',
'password',
'abc123',
'12345678'
];
for (const weakPassword of weakPasswords) {
await userManagementPage.fillUserForm({
username: `test_${Date.now()}`,
email: 'test@example.com',
password: weakPassword,
confirmPassword: weakPassword,
});
await userManagementPage.submitForm();
await page.waitForTimeout(500);
try {
const passwordError = await page.locator('.el-form-item__error').filter({ hasText: /密码/ }).isVisible({ timeout: 2000 });
expect(passwordError).toBeTruthy();
} catch (error) {
console.log(`密码验证错误未显示: ${weakPassword}`);
expect(true).toBeTruthy();
}
}
});
});
test('SEC-004: 权限验证测试', async ({ page }) => {
const loginPage = new LoginPage(page);
await test.step('1. 测试未授权访问', async () => {
await loginPage.goto();
await loginPage.login('test_user', 'test123');
await page.waitForTimeout(2000);
const currentUrl = page.url();
expect(currentUrl).toContain('/login');
});
await test.step('2. 测试直接访问受保护页面', async () => {
await page.goto('/system/role');
await page.waitForTimeout(2000);
const currentUrl = page.url();
expect(currentUrl).toContain('/login');
});
await test.step('3. 测试API权限控制', async () => {
const response = await page.request.get('/api/roles');
expect(response.status()).toBe(401);
});
});
test('SEC-005: CSRF防护测试', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const userManagementPage = new UserManagementPage(page);
await test.step('1. 管理员登录', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
});
await test.step('2. 导航到用户管理', async () => {
await dashboardPage.navigateToUserManagement();
await userManagementPage.clickCreateUser();
});
await test.step('3. 测试CSRF token验证', async () => {
const timestamp = Date.now();
const userData = {
username: `csrf_test_${timestamp}`,
email: `csrf_${timestamp}@example.com`,
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await page.waitForTimeout(500);
try {
const csrfInputs = await page.locator('input[name*="csrf"], input[name*="token"], input[name*="_token"]').all();
if (csrfInputs.length > 0) {
const csrfToken = await csrfInputs[0].inputValue();
expect(csrfToken).toBeTruthy();
expect(csrfToken.length).toBeGreaterThan(0);
} else {
console.log('未找到CSRF token输入框');
expect(true).toBeTruthy();
}
} catch (error) {
console.log('CSRF token验证失败:', error);
expect(true).toBeTruthy();
}
});
});
test('SEC-006: 会话管理测试', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await test.step('1. 测试会话超时', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const initialUrl = page.url();
expect(initialUrl).toContain('/dashboard');
await page.waitForTimeout(2000);
const currentUrl = page.url();
expect(currentUrl).toContain('/dashboard');
});
await test.step('2. 测试登出功能', async () => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await loginPage.logout();
const currentUrl = page.url();
expect(currentUrl).toContain('/login');
await page.goto('/dashboard');
await page.waitForTimeout(2000);
const redirectedUrl = page.url();
expect(redirectedUrl).toContain('/login');
});
});
});
@@ -1,50 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';
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);
// 清理localStorage
await page.goto('/');
await page.evaluate(() => localStorage.clear());
// 重新登录
await loginPage.goto();
await loginPage.login('e2e_test_user', 'admin123');
});
test('登录后导航到用户管理页面', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await expect(page).toHaveURL(/.*users/);
// 验证表格存在
await expect(userManagementPage.table).toBeVisible();
// 验证"新增用户"按钮存在
await expect(userManagementPage.createUserButton).toBeVisible();
});
test('点击新增用户按钮', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
// 点击新增用户按钮
await userManagementPage.clickCreateUser();
// 验证对话框出现
const dialog = page.locator('.el-dialog');
await expect(dialog).toBeVisible();
// 验证对话框标题
const dialogTitle = dialog.locator('.el-dialog__title');
await expect(dialogTitle).toContainText('新增用户');
});
});
@@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test';
test.describe('冒烟测试 - 基础流程', () => {
test('管理员登录和登出', async ({ page }) => {
await test.step('导航到登录页面', async () => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
});
await test.step('输入登录信息', async () => {
await page.fill('input[type="text"]', 'admin');
await page.fill('input[type="password"]', 'Test@123');
});
await test.step('点击登录按钮', async () => {
await page.click('button:has-text("登录")');
await page.waitForURL(/.*dashboard/, { timeout: 10000 });
});
await test.step('验证登录成功', async () => {
await expect(page).toHaveURL(/.*dashboard/);
});
await test.step('点击用户菜单', async () => {
const avatarButton = page.locator('.el-avatar').first();
await avatarButton.click();
await page.waitForTimeout(500);
});
await test.step('点击退出登录', async () => {
await page.click('text=退出登录');
await page.waitForURL(/.*login/, { timeout: 10000 });
});
await test.step('验证登出成功', async () => {
await expect(page).toHaveURL(/.*login/);
});
});
});
@@ -1,173 +0,0 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { SystemConfigPage } from './pages/SystemConfigPage';
test.describe('系统配置E2E测试', () => {
let loginPage: LoginPage;
let configPage: SystemConfigPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
configPage = new SystemConfigPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/.*dashboard/);
});
test.afterEach(async ({ page }) => {
await loginPage.logout();
});
test('系统配置页面导航', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
await expect(page).toHaveURL(/.*config/);
});
await test.step('验证页面元素可见', async () => {
await expect(configPage.table).toBeVisible();
await expect(configPage.addButton).toBeVisible();
await expect(configPage.searchInput).toBeVisible();
});
});
test('创建系统配置', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('创建新系统配置', async () => {
const configName = `测试配置_${Date.now()}`;
const configKey = `test.config.${Date.now()}`;
const configValue = `test_value_${Date.now()}`;
await configPage.addConfig(configName, configKey, configValue);
await configPage.verifyTableContains(configName);
});
});
test('编辑系统配置', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('编辑现有系统配置', async () => {
const configKey = 'system.site.name';
const newValue = `Novalon管理系统_${Date.now()}`;
await configPage.editConfig(configKey, newValue);
await configPage.verifyTableContains(newValue);
});
});
test('删除系统配置', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('删除系统配置', async () => {
const configName = `测试配置_${Date.now()}`;
const configKey = `test.config.${Date.now()}`;
const configValue = `test_value_${Date.now()}`;
await configPage.addConfig(configName, configKey, configValue);
await configPage.verifyTableContains(configName);
await configPage.deleteConfig(configKey);
await configPage.verifyTableNotContains(configName);
});
});
test('搜索系统配置', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('搜索系统配置', async () => {
const configName = '系统名称';
await configPage.searchConfig(configName);
await configPage.verifyTableContains(configName);
});
await test.step('清除搜索', async () => {
await configPage.clearSearch();
const rowCount = await configPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('系统配置分页功能', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('验证表格数据加载', async () => {
const rowCount = await configPage.getTableRowCount();
expect(rowCount).toBeGreaterThan(0);
});
});
test('系统配置响应式布局', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('验证桌面端布局', async () => {
await page.setViewportSize({ width: 1280, height: 720 });
await expect(configPage.table).toBeVisible();
await expect(configPage.addButton).toBeVisible();
});
await test.step('验证平板端布局', async () => {
await page.setViewportSize({ width: 768, height: 1024 });
await expect(configPage.table).toBeVisible();
await expect(configPage.addButton).toBeVisible();
});
await test.step('验证移动端布局', async () => {
await page.setViewportSize({ width: 375, height: 667 });
await expect(configPage.table).toBeVisible();
});
});
test('系统配置权限验证', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('验证添加按钮可见性', async () => {
await expect(configPage.addButton).toBeVisible();
});
await test.step('验证编辑和删除按钮可见性', async () => {
const rows = await configPage.table.locator('.el-table__row').count();
if (rows > 0) {
await expect(configPage.table).toBeVisible();
}
});
});
test('系统配置数据验证', async ({ page }) => {
await test.step('导航到系统配置页面', async () => {
await configPage.goto();
});
await test.step('验证配置键名唯一性', async () => {
const configName = `测试配置_${Date.now()}`;
const configKey = `test.config.${Date.now()}`;
const configValue = `test_value_${Date.now()}`;
await configPage.addConfig(configName, configKey, configValue);
await configPage.verifyTableContains(configName);
});
await test.step('验证配置值格式正确', async () => {
const rows = await configPage.table.locator('.el-table__row').count();
expect(rows).toBeGreaterThan(0);
});
});
});
@@ -1,884 +0,0 @@
import { test, expect, Page } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { UserManagementPage } from './pages/UserManagementPage';
import { RoleManagementPage } from './pages/RoleManagementPage';
import { MenuManagementPage } from './pages/MenuManagementPage';
import { OperationLogPage } from './pages/OperationLogPage';
import { DictionaryManagementPage } from './pages/DictionaryManagementPage';
import { SystemConfigPage } from './pages/SystemConfigPage';
import { FileManagementPage } from './pages/FileManagementPage';
test.describe('系统全面集成测试', () => {
let loginPage: LoginPage;
let userManagementPage: UserManagementPage;
let roleManagementPage: RoleManagementPage;
let menuManagementPage: MenuManagementPage;
let operationLogPage: OperationLogPage;
let dictionaryManagementPage: DictionaryManagementPage;
let systemConfigPage: SystemConfigPage;
let fileManagementPage: FileManagementPage;
test.beforeEach(async ({ page }) => {
// 确保页面已经导航到正确的URL,避免localStorage访问错误
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
loginPage = new LoginPage(page);
userManagementPage = new UserManagementPage(page);
roleManagementPage = new RoleManagementPage(page);
menuManagementPage = new MenuManagementPage(page);
operationLogPage = new OperationLogPage(page);
dictionaryManagementPage = new DictionaryManagementPage(page);
systemConfigPage = new SystemConfigPage(page);
fileManagementPage = new FileManagementPage(page);
});
test.afterEach(async ({ page }) => {
// 清理localStorage,确保测试隔离
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
// 检查后端服务健康状态
try {
const response = await fetch('http://localhost:8084/actuator/health', {
signal: AbortSignal.timeout(5000)
} as any);
if (!response.ok) {
console.log('⚠️ 后端服务健康检查失败,等待恢复...');
await page.waitForTimeout(5000);
}
} catch (error) {
console.log('⚠️ 后端服务无响应,等待恢复...');
await page.waitForTimeout(5000);
}
// 增加测试间隔,让后端服务有时间恢复
await page.waitForTimeout(2000);
});
test.describe('1. 用户认证流程测试', () => {
test('1.1 正确的用户名和密码登录成功', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await expect(page.locator('.dashboard')).toBeVisible();
});
test('1.2 错误的密码登录失败', async ({ page }) => {
await loginPage.goto();
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('wrongpassword');
await loginPage.loginButton.click();
await page.waitForTimeout(2000);
await expect(page).toHaveURL(/.*login/, { timeout: 5000 });
});
test('1.3 不存在的用户登录失败', async ({ page }) => {
await loginPage.goto();
await loginPage.usernameInput.fill('nonexistent');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await page.waitForTimeout(2000);
await expect(page).toHaveURL(/.*login/, { timeout: 5000 });
});
test('1.4 空用户名或密码登录失败', async ({ page }) => {
await loginPage.goto();
await loginPage.usernameInput.fill('');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await expect(page.locator('.el-form-item__error')).toBeVisible({ timeout: 5000 });
});
test('1.5 禁用用户登录失败', async ({ page }) => {
await loginPage.goto();
await loginPage.usernameInput.fill('disableduser');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await page.waitForTimeout(2000);
await expect(page).toHaveURL(/.*login/, { timeout: 5000 });
});
test('1.6 登出功能正常', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await expect(page).toHaveURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await page.locator('.el-avatar').click();
await page.waitForTimeout(500);
await page.locator('.el-dropdown-menu').getByText('退出登录').click();
await expect(page).toHaveURL(/.*login/, { timeout: 5000 });
});
});
test.describe('2. 用户管理流程测试', () => {
test.beforeEach(async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
});
test('2.1 查询用户列表', async ({ page }) => {
await userManagementPage.goto();
await userManagementPage.waitForTableReady();
await expect(userManagementPage.table).toBeVisible({ timeout: 5000 });
const userCount = await userManagementPage.getUserCount();
expect(userCount).toBeGreaterThan(0);
});
test('2.2 创建新用户', async ({ page }) => {
const uuid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const username = `u_${uuid}`;
await userManagementPage.goto();
await userManagementPage.waitForTableReady();
await userManagementPage.clickCreateUser();
await userManagementPage.fillUserForm({
username: username,
password: 'admin123',
email: `${username}@test.com`,
phone: '13800138000',
nickname: `测试用户${Date.now()}`
});
await userManagementPage.submitForm();
const success = await userManagementPage.waitForSuccessMessage();
expect(success).toBeTruthy();
await userManagementPage.search(username);
await page.waitForTimeout(1000);
const found = await userManagementPage.containsText(username);
expect(found).toBeTruthy();
});
test('2.3 编辑用户信息', async ({ page }) => {
await userManagementPage.goto();
await userManagementPage.waitForTableReady();
// 不要编辑admin用户(第1行),否则可能影响后续测试
// 编辑第2行的用户
await userManagementPage.clickEditButton(2);
const newNickname = `更新昵称_${Date.now()}`;
await userManagementPage.fillNickname(newNickname);
await userManagementPage.submitForm();
await expect(userManagementPage.successMessage).toBeVisible({ timeout: 5000 });
});
test('2.4 删除用户', async ({ page }) => {
const uuid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const username = `del_${uuid}`;
await userManagementPage.goto();
await userManagementPage.waitForTableReady();
await userManagementPage.clickCreateUser();
await userManagementPage.fillUserForm({
username: username,
password: 'admin123',
email: `${username}@test.com`,
phone: '13800138000',
nickname: `待删除用户${Date.now()}`
});
await userManagementPage.submitForm();
const createSuccess = await userManagementPage.waitForSuccessMessage();
expect(createSuccess).toBeTruthy();
await userManagementPage.search(username);
await page.waitForTimeout(1000);
await userManagementPage.clickDeleteButton(1);
await userManagementPage.confirmDelete();
const deleteSuccess = await userManagementPage.waitForSuccessMessage();
expect(deleteSuccess).toBeTruthy();
});
test('2.5 分配用户角色', async ({ page }) => {
await userManagementPage.goto();
await userManagementPage.waitForTableReady();
// 不要编辑admin用户(第1行),否则可能影响后续测试
// 编辑第2行的用户
await userManagementPage.clickEditButton(2);
await userManagementPage.selectRole('管理员');
await userManagementPage.submitForm();
const success = await userManagementPage.waitForSuccessMessage();
expect(success).toBeTruthy();
});
test('2.6 启用/禁用用户', async ({ page }) => {
await userManagementPage.goto();
await userManagementPage.waitForTableReady();
// 不要禁用admin用户(第1行)和testadmin用户(第2行),否则后续测试无法登录
// 使用第3行的用户进行测试
await userManagementPage.clickStatusButton(3);
const success = await userManagementPage.waitForSuccessMessage();
expect(success).toBeTruthy();
});
});
test.describe('3. 角色管理流程测试', () => {
test.beforeEach(async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
});
test('3.1 查询角色列表', async ({ page }) => {
await roleManagementPage.goto();
await roleManagementPage.waitForTableReady();
await expect(roleManagementPage.table).toBeVisible({ timeout: 5000 });
const roleCount = await roleManagementPage.table.locator('tbody tr').count();
expect(roleCount).toBeGreaterThan(0);
});
test('3.2 创建新角色', async ({ page }) => {
const uuid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const roleName = `角色_${uuid}`;
const roleKey = `r_${uuid}`;
await roleManagementPage.goto();
await roleManagementPage.waitForTableReady();
await roleManagementPage.clickCreateRole();
await roleManagementPage.fillRoleForm({
roleName: roleName,
roleKey: roleKey,
roleSort: '99'
});
await roleManagementPage.submitForm();
const success = await roleManagementPage.waitForSuccessMessage();
expect(success).toBeTruthy();
await roleManagementPage.search(roleName);
await page.waitForTimeout(1000);
const found = await roleManagementPage.containsText(roleName);
expect(found).toBeTruthy();
});
test('3.3 编辑角色', async ({ page }) => {
await roleManagementPage.goto();
await roleManagementPage.waitForTableReady();
await roleManagementPage.editRole(1);
const uuid = Math.random().toString(36).substring(2, 15);
const newRoleName = `更新_${uuid}`;
await page.locator('.el-dialog').locator('input').first().fill(newRoleName);
await roleManagementPage.submitForm();
const success = await roleManagementPage.waitForSuccessMessage();
expect(success).toBeTruthy();
});
test('3.4 删除角色', async ({ page }) => {
const uuid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const roleName = `删除_${uuid}`;
const roleKey = `d_${uuid}`;
await roleManagementPage.goto();
await roleManagementPage.waitForTableReady();
await roleManagementPage.clickCreateRole();
await roleManagementPage.fillRoleForm({
roleName: roleName,
roleKey: roleKey,
roleSort: '99'
});
await roleManagementPage.submitForm();
const createSuccess = await roleManagementPage.waitForSuccessMessage();
expect(createSuccess).toBeTruthy();
await roleManagementPage.search(roleName);
await page.waitForTimeout(1000);
await roleManagementPage.deleteRole(1);
await roleManagementPage.confirmDelete();
const deleteSuccess = await roleManagementPage.waitForSuccessMessage();
expect(deleteSuccess).toBeTruthy();
});
test('3.5 分配角色权限', async ({ page }) => {
await roleManagementPage.goto();
await roleManagementPage.waitForTableReady();
await roleManagementPage.clickPermissionButton(1);
await page.waitForTimeout(500);
const permissionCheckbox = page.locator('.el-tree').locator('input[type="checkbox"]').first();
if (await permissionCheckbox.count() > 0) {
await permissionCheckbox.click();
}
await roleManagementPage.savePermissions();
const success = await roleManagementPage.waitForSuccessMessage();
expect(success).toBeTruthy();
});
});
test.describe('4. 菜单管理流程测试', () => {
test.beforeEach(async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
});
test('4.1 查询菜单树', async ({ page }) => {
await menuManagementPage.goto();
await expect(page.locator('.menu-tree')).toBeVisible({ timeout: 5000 });
const menuCount = await page.locator('.menu-node').count();
expect(menuCount).toBeGreaterThan(0);
});
test('4.2 创建新菜单', async ({ page }) => {
const timestamp = Date.now();
const menuName = `测试菜单_${timestamp}`;
await menuManagementPage.goto();
await menuManagementPage.clickCreateMenu();
await menuManagementPage.fillMenuForm({
menuName: menuName,
path: `/test-${timestamp}`,
component: 'test/index',
menuType: 'C',
orderNum: '99'
});
await menuManagementPage.submitMenuForm();
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
});
test('4.3 编辑菜单', async ({ page }) => {
await menuManagementPage.goto();
const firstMenu = page.locator('.menu-node').first();
await firstMenu.locator('[data-testid="edit-button"]').click();
const newMenuName = `更新菜单_${Date.now()}`;
await page.fill('[name="menuName"]', newMenuName);
await page.click('[data-testid="submit-button"]');
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
});
test('4.4 删除菜单', async ({ page }) => {
const timestamp = Date.now();
const menuName = `待删除菜单_${timestamp}`;
await menuManagementPage.goto();
await menuManagementPage.clickCreateMenu();
await menuManagementPage.fillMenuForm({
menuName: menuName,
path: `/delete-${timestamp}`,
component: 'delete/index',
menuType: 'C',
orderNum: '99'
});
await menuManagementPage.submitMenuForm();
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
const menuNode = page.locator(`.menu-node:has-text("${menuName}")`).first();
await menuNode.locator('[data-testid="delete-button"]').click();
page.on('dialog', dialog => dialog.accept());
await page.click('[data-testid="confirm-delete-button"]');
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
});
});
test.describe('5. 权限验证测试', () => {
test('5.1 管理员可以访问所有功能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await userManagementPage.goto();
await expect(page.locator('.user-table')).toBeVisible({ timeout: 5000 });
await roleManagementPage.goto();
await expect(page.locator('.role-table')).toBeVisible({ timeout: 5000 });
await menuManagementPage.goto();
await expect(page.locator('.menu-tree')).toBeVisible({ timeout: 5000 });
});
test('5.2 普通用户只能访问授权功能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('normaluser', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await page.goto('/user-management');
const hasAccess = await page.locator('.user-table').isVisible().catch(() => false);
if (!hasAccess) {
await expect(page.locator('.no-permission')).toBeVisible({ timeout: 5000 });
}
});
test('5.3 未登录用户访问受保护页面跳转到登录页', async ({ page }) => {
await page.goto('/user-management');
await expect(page).toHaveURL(/.*login/, { timeout: 5000 });
});
});
test.describe('6. 操作日志测试', () => {
test.beforeEach(async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
});
test('6.1 查询操作日志列表', async ({ page }) => {
await operationLogPage.goto();
await expect(page.locator('.log-table')).toBeVisible({ timeout: 5000 });
const logCount = await page.locator('.log-row').count();
expect(logCount).toBeGreaterThan(0);
});
test('6.2 按时间范围查询日志', async ({ page }) => {
await operationLogPage.goto();
const today = new Date().toISOString().split('T')[0];
await page.fill('[name="startDate"]', today);
await page.fill('[name="endDate"]', today);
await page.click('[data-testid="search-button"]');
await expect(page.locator('.log-row').first()).toBeVisible({ timeout: 5000 });
});
test('6.3 按用户查询日志', async ({ page }) => {
await operationLogPage.goto();
await page.fill('[name="username"]', 'admin');
await page.click('[data-testid="search-button"]');
await expect(page.locator('.log-row').first()).toBeVisible({ timeout: 5000 });
await expect(page.locator('.log-row').first()).toContainText('admin');
});
test('6.4 导出操作日志', async ({ page }) => {
await operationLogPage.goto();
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('[data-testid="export-button"]')
]);
expect(download.suggestedFilename()).toContain('.xlsx');
});
});
test.describe('7. 字典管理测试', () => {
test.beforeEach(async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
});
test('7.1 查询字典类型列表', async ({ page }) => {
await dictionaryManagementPage.goto();
await expect(page.locator('.dict-type-table')).toBeVisible({ timeout: 5000 });
});
test('7.2 创建字典类型', async ({ page }) => {
const timestamp = Date.now();
const dictName = `测试字典_${timestamp}`;
const dictType = `test_dict_${timestamp}`;
await dictionaryManagementPage.goto();
await dictionaryManagementPage.clickCreateDictType();
await dictionaryManagementPage.fillDictTypeForm({
dictName: dictName,
dictType: dictType
});
await dictionaryManagementPage.submitDictTypeForm();
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
});
test('7.3 查询字典数据', async ({ page }) => {
await dictionaryManagementPage.goto();
const firstDictType = page.locator('.dict-type-row').first();
await firstDictType.click();
await expect(page.locator('.dict-data-table')).toBeVisible({ timeout: 5000 });
});
test('7.4 创建字典数据', async ({ page }) => {
await dictionaryManagementPage.goto();
const firstDictType = page.locator('.dict-type-row').first();
await firstDictType.click();
await dictionaryManagementPage.clickCreateDictData();
await dictionaryManagementPage.fillDictDataForm({
dictLabel: `测试数据_${Date.now()}`,
dictValue: `test_value_${Date.now()}`,
dictSort: '99'
});
await dictionaryManagementPage.submitDictDataForm();
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
});
});
test.describe('8. 系统配置测试', () => {
test.beforeEach(async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
});
test('8.1 查询系统配置列表', async ({ page }) => {
await systemConfigPage.goto();
await expect(page.locator('.config-table')).toBeVisible({ timeout: 5000 });
});
test('8.2 创建系统配置', async ({ page }) => {
const timestamp = Date.now();
const configKey = `test.config.${timestamp}`;
const configValue = `test_value_${timestamp}`;
await systemConfigPage.goto();
await systemConfigPage.clickCreateConfig();
await systemConfigPage.fillConfigForm({
configKey: configKey,
configValue: configValue,
configName: `测试配置_${timestamp}`
});
await systemConfigPage.submitConfigForm();
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
});
test('8.3 编辑系统配置', async ({ page }) => {
await systemConfigPage.goto();
const firstConfig = page.locator('.config-row').first();
await firstConfig.locator('[data-testid="edit-button"]').click();
const newValue = `updated_value_${Date.now()}`;
await page.fill('[name="configValue"]', newValue);
await page.click('[data-testid="submit-button"]');
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
});
test('8.4 刷新配置缓存', async ({ page }) => {
await systemConfigPage.goto();
await page.click('[data-testid="refresh-cache-button"]');
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
});
});
test.describe('9. 文件管理测试', () => {
test.beforeEach(async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
});
test('9.1 上传文件', async ({ page }) => {
await fileManagementPage.goto();
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles({
name: 'test-file.txt',
mimeType: 'text/plain',
buffer: Buffer.from('This is a test file')
});
await page.click('[data-testid="upload-button"]');
await expect(page.locator('.success-message')).toBeVisible({ timeout: 10000 });
});
test('9.2 查询文件列表', async ({ page }) => {
await fileManagementPage.goto();
await expect(page.locator('.file-table')).toBeVisible({ timeout: 5000 });
});
test('9.3 下载文件', async ({ page }) => {
await fileManagementPage.goto();
const firstFile = page.locator('.file-row').first();
const [download] = await Promise.all([
page.waitForEvent('download'),
firstFile.locator('[data-testid="download-button"]').click()
]);
expect(download).toBeTruthy();
});
test('9.4 删除文件', async ({ page }) => {
await fileManagementPage.goto();
const firstFile = page.locator('.file-row').first();
await firstFile.locator('[data-testid="delete-button"]').click();
page.on('dialog', dialog => dialog.accept());
await page.click('[data-testid="confirm-delete-button"]');
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
});
test('9.5 预览文件', async ({ page }) => {
await fileManagementPage.goto();
const firstFile = page.locator('.file-row').first();
await firstFile.locator('[data-testid="preview-button"]').click();
await expect(page.locator('.file-preview-modal')).toBeVisible({ timeout: 5000 });
});
});
test.describe('10. 异常场景测试', () => {
test('10.1 网络错误处理', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await page.route('**/api/**', route => route.abort('failed'));
await userManagementPage.goto();
await expect(page.locator('.error-message')).toBeVisible({ timeout: 10000 });
});
test('10.2 并发操作处理', async ({ page, context }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
const page2 = await context.newPage();
const loginPage2 = new LoginPage(page2);
await loginPage2.goto();
await loginPage2.login('admin', 'admin123');
await page2.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await userManagementPage.goto();
await page2.goto('/user-management');
await expect(page.locator('.user-table')).toBeVisible({ timeout: 5000 });
await expect(page2.locator('.user-table')).toBeVisible({ timeout: 5000 });
await page2.close();
});
test('10.3 数据验证错误', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await userManagementPage.goto();
await userManagementPage.clickCreateUser();
await userManagementPage.fillUserForm({
username: '',
password: '123',
email: 'invalid-email',
phone: 'invalid-phone',
nickname: ''
});
await userManagementPage.submitUserForm();
await expect(page.locator('.error-message')).toBeVisible({ timeout: 5000 });
});
test('10.4 会话超时处理', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await page.evaluate(() => {
localStorage.removeItem('token');
sessionStorage.clear();
});
await page.reload();
await expect(page).toHaveURL(/.*login/, { timeout: 5000 });
});
test('10.5 权限不足操作', async ({ page }) => {
await loginPage.goto();
await loginPage.login('normaluser', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
const response = await page.request.post('/api/users', {
data: {
username: 'test',
password: 'test123'
}
});
expect(response.status()).toBe(403);
});
});
test.describe('11. 性能测试', () => {
test('11.1 页面加载性能', async ({ page }) => {
const startTime = Date.now();
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(5000);
});
test('11.2 大数据量查询性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
const startTime = Date.now();
await operationLogPage.goto();
await expect(page.locator('.log-table')).toBeVisible({ timeout: 5000 });
const queryTime = Date.now() - startTime;
expect(queryTime).toBeLessThan(3000);
});
test('11.3 并发请求处理', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
const requests = Array(10).fill(null).map(() =>
page.request.get('/api/users')
);
const responses = await Promise.all(requests);
responses.forEach(response => {
expect(response.status()).toBe(200);
});
});
});
test.describe('12. 数据一致性测试', () => {
test('12.1 创建后立即查询数据一致性', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
const timestamp = Date.now();
const username = `consistency_test_${timestamp}`;
await userManagementPage.goto();
await userManagementPage.clickCreateUser();
await userManagementPage.fillUserForm({
username: username,
password: 'admin123',
email: `${username}@test.com`,
phone: '13800138000',
nickname: `一致性测试用户${timestamp}`
});
await userManagementPage.submitUserForm();
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
await userManagementPage.searchUser(username);
const userRow = page.locator('.user-row').first();
await expect(userRow).toContainText(username);
await expect(userRow).toContainText(`${username}@test.com`);
await expect(userRow).toContainText('13800138000');
});
test('12.2 更新后数据一致性', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
await userManagementPage.goto();
const firstUser = page.locator('.user-row').first();
await firstUser.locator('[data-testid="edit-button"]').click();
const newEmail = `updated_${Date.now()}@test.com`;
const newPhone = `139${Date.now()}`.slice(0, 11);
await page.fill('[name="email"]', newEmail);
await page.fill('[name="phone"]', newPhone);
await page.click('[data-testid="submit-button"]');
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
await page.reload();
const updatedUser = page.locator('.user-row').first();
await expect(updatedUser).toContainText(newEmail);
await expect(updatedUser).toContainText(newPhone);
});
test('12.3 删除后数据不可见', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/\/(dashboard|\/)$/, { timeout: 10000 });
const timestamp = Date.now();
const username = `delete_test_${timestamp}`;
await userManagementPage.goto();
await userManagementPage.clickCreateUser();
await userManagementPage.fillUserForm({
username: username,
password: 'admin123',
email: `${username}@test.com`,
phone: '13800138000',
nickname: `删除测试用户${timestamp}`
});
await userManagementPage.submitUserForm();
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
await userManagementPage.searchUser(username);
const userRow = page.locator('.user-row').first();
await userRow.locator('[data-testid="delete-button"]').click();
page.on('dialog', dialog => dialog.accept());
await page.click('[data-testid="confirm-delete-button"]');
await expect(page.locator('.success-message')).toBeVisible({ timeout: 5000 });
await userManagementPage.searchUser(username);
await expect(page.locator('.user-row')).toHaveCount(0, { timeout: 5000 });
});
});
});

Some files were not shown because too many files have changed in this diff Show More