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 password: novalon123
flyway: flyway:
enabled: true enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
rate: rate:
limit: 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: application:
name: manage-app name: manage-app
r2dbc: r2dbc:
url: r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE url: r2dbc:postgresql://localhost:55432/manage_system
username: sa username: novalon
password: password: novalon123
pool: pool:
initial-size: 5 initial-size: 5
max-size: 20 max-size: 20
max-idle-time: 30m max-idle-time: 30m
max-life-time: 1h max-life-time: 1h
acquire-timeout: 5s 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: flyway:
enabled: false
h2:
console:
enabled: true enabled: true
path: /h2-console locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
sql:
init:
mode: never
security: security:
user: user:
name: disabled 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 VALUES
(1, '超级管理员', 'admin', 1, 1, 'system', 'system'), (1, '超级管理员', 'admin', 1, 1, 'system', 'system'),
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system'), (2, '测试管理员', 'test_admin', 2, 1, 'system', 'system'),
@@ -11,7 +12,8 @@ VALUES
-- 插入测试用户 -- 插入测试用户
-- BCrypt哈希值对应明文密码: Test@123 -- 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 VALUES
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'), (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'), (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; package cn.novalon.manage.app.integration;
import cn.novalon.manage.app.config.TestDatabaseConfig;
import cn.novalon.manage.common.util.StatusConstants; import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.core.domain.SysUser; import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.domain.SysRole; 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.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
@@ -27,7 +25,7 @@ import static org.junit.jupiter.api.Assertions.*;
/** /**
* 用户服务集成测试 * 用户服务集成测试
* *
* 使用H2内存数据库进行集成测试 * 使用PostgreSQL数据库进行集成测试
* *
* 注意:此测试需要完整的Spring上下文,暂时禁用。 * 注意:此测试需要完整的Spring上下文,暂时禁用。
* TODO: 优化集成测试配置 * TODO: 优化集成测试配置
@@ -38,7 +36,6 @@ import static org.junit.jupiter.api.Assertions.*;
@Disabled("暂时禁用:集成测试配置需要优化") @Disabled("暂时禁用:集成测试配置需要优化")
@SpringBootTest @SpringBootTest
@ActiveProfiles("test") @ActiveProfiles("test")
@Import(TestDatabaseConfig.class)
class SysUserServiceIntegrationTest { class SysUserServiceIntegrationTest {
@Autowired @Autowired
@@ -1,27 +1,22 @@
spring: spring:
r2dbc: r2dbc:
url: r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE url: r2dbc:postgresql://localhost:55432/manage_system
username: sa username: novalon
password: password: novalon123
pool: pool:
enabled: true enabled: true
initial-size: 2 initial-size: 2
max-size: 10 max-size: 10
h2: flyway:
console:
enabled: true enabled: true
path: /h2-console locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
sql: sql:
init: init:
mode: always mode: never
continue-on-error: false
schema-locations: classpath:schema-h2.sql
data-locations: classpath:data-h2.sql
flyway:
enabled: false
security: security:
enabled: false 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_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_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> <dependency>
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>
<artifactId>h2</artifactId> <artifactId>h2</artifactId>
<scope>test</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.r2dbc</groupId> <groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId> <artifactId>r2dbc-h2</artifactId>
<scope>test</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.flywaydb</groupId> <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; 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.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 org.springframework.data.relational.core.mapping.Column;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -11,26 +16,31 @@ import java.time.LocalDateTime;
* @author 张翔 * @author 张翔
* @date 2026-03-13 * @date 2026-03-13
*/ */
public abstract class BaseEntity { public abstract class BaseEntity implements Persistable<Long> {
@Id @Id
private Long id; private Long id;
@CreatedBy
@Column("create_by") @Column("create_by")
private String createBy; private String createBy;
@LastModifiedBy
@Column("update_by") @Column("update_by")
private String updateBy; private String updateBy;
@CreatedDate
@Column("created_at") @Column("created_at")
private LocalDateTime createdAt; private LocalDateTime createdAt;
@LastModifiedDate
@Column("updated_at") @Column("updated_at")
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
@Column("deleted_at") @Column("deleted_at")
private LocalDateTime deletedAt; private LocalDateTime deletedAt;
@Override
public Long getId() { public Long getId() {
return id; return id;
} }
@@ -78,4 +88,13 @@ public abstract class BaseEntity {
public void setDeletedAt(LocalDateTime deletedAt) { public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = 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 ( CREATE TABLE IF NOT EXISTS sys_user (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE, username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
email VARCHAR(100), email VARCHAR(100),
@@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS sys_user (
); );
-- 角色表 -- 角色表
CREATE TABLE IF NOT EXISTS sys_role ( CREATE TABLE IF NOT EXISTS sys_role (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
role_name VARCHAR(100) NOT NULL, role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE, role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0, role_sort INTEGER DEFAULT 0,
@@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS sys_role (
); );
-- 菜单表(统一使用sys_menu表名) -- 菜单表(统一使用sys_menu表名)
CREATE TABLE IF NOT EXISTS sys_menu ( CREATE TABLE IF NOT EXISTS sys_menu (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL, menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0, parent_id BIGINT DEFAULT 0,
order_num INTEGER 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 ( CREATE TABLE IF NOT EXISTS sys_dict_type (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
dict_name VARCHAR(100) NOT NULL, dict_name VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL UNIQUE, dict_type VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(1) DEFAULT '0', 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 ( CREATE TABLE IF NOT EXISTS sys_dict_data (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
dict_sort INTEGER DEFAULT 0, dict_sort INTEGER DEFAULT 0,
dict_label VARCHAR(100) NOT NULL, dict_label VARCHAR(100) NOT NULL,
dict_value 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 ( CREATE TABLE IF NOT EXISTS sys_dictionary (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
type VARCHAR(100) NOT NULL, type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL, code VARCHAR(100) NOT NULL,
name 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 ( CREATE TABLE IF NOT EXISTS sys_config (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
config_name VARCHAR(100) NOT NULL, config_name VARCHAR(100) NOT NULL,
config_key VARCHAR(100) NOT NULL UNIQUE, config_key VARCHAR(100) NOT NULL UNIQUE,
config_value VARCHAR(500) NOT NULL, 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 ( CREATE TABLE IF NOT EXISTS sys_login_log (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
username VARCHAR(50), username VARCHAR(50),
ip VARCHAR(50), ip VARCHAR(50),
location VARCHAR(255), location VARCHAR(255),
@@ -117,7 +117,7 @@ CREATE TABLE IF NOT EXISTS sys_login_log (
); );
-- 异常日志表 -- 异常日志表
CREATE TABLE IF NOT EXISTS sys_exception_log ( CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
username VARCHAR(50), username VARCHAR(50),
title VARCHAR(100), title VARCHAR(100),
exception_name 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 ( CREATE TABLE IF NOT EXISTS operation_log (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
username VARCHAR(50), username VARCHAR(50),
operation VARCHAR(100), operation VARCHAR(100),
method VARCHAR(200), method VARCHAR(200),
@@ -148,7 +148,7 @@ CREATE TABLE IF NOT EXISTS operation_log (
); );
-- 系统公告表 -- 系统公告表
CREATE TABLE IF NOT EXISTS sys_notice ( CREATE TABLE IF NOT EXISTS sys_notice (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
notice_title VARCHAR(50) NOT NULL, notice_title VARCHAR(50) NOT NULL,
notice_type VARCHAR(1) NOT NULL, notice_type VARCHAR(1) NOT NULL,
notice_content TEXT, notice_content TEXT,
@@ -161,7 +161,7 @@ CREATE TABLE IF NOT EXISTS sys_notice (
); );
-- 用户消息表 -- 用户消息表
CREATE TABLE IF NOT EXISTS sys_user_message ( CREATE TABLE IF NOT EXISTS sys_user_message (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
notice_id BIGINT, notice_id BIGINT,
message_title VARCHAR(255), message_title VARCHAR(255),
@@ -176,7 +176,7 @@ CREATE TABLE IF NOT EXISTS sys_user_message (
); );
-- 文件管理表 -- 文件管理表
CREATE TABLE IF NOT EXISTS sys_file ( CREATE TABLE IF NOT EXISTS sys_file (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
file_name VARCHAR(255) NOT NULL, file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL, file_path VARCHAR(500) NOT NULL,
file_size BIGINT, file_size BIGINT,
@@ -191,7 +191,7 @@ CREATE TABLE IF NOT EXISTS sys_file (
); );
-- OAuth2客户端表 -- OAuth2客户端表
CREATE TABLE IF NOT EXISTS oauth2_client ( CREATE TABLE IF NOT EXISTS oauth2_client (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
client_id VARCHAR(100) NOT NULL UNIQUE, client_id VARCHAR(100) NOT NULL UNIQUE,
client_secret VARCHAR(255) NOT NULL, client_secret VARCHAR(255) NOT NULL,
client_name VARCHAR(100), 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') VALUES ('超级管理员', 'admin', 1, 1, 'system', 'system')
ON CONFLICT (role_key) DO NOTHING; ON CONFLICT (role_key) DO NOTHING;
-- 插入初始管理员用户 -- 插入初始管理员用户
-- BCrypt哈希值对应明文密码: admin123 -- 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') VALUES (1, 'admin', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'admin@novalon.com', '13800138000', 1, 'system', 'system')
ON CONFLICT (username) DO UPDATE SET ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password, password = EXCLUDED.password,
status = EXCLUDED.status; 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) INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by)
VALUES VALUES
@@ -52,8 +60,8 @@ VALUES
ON CONFLICT (config_key) DO NOTHING; ON CONFLICT (config_key) DO NOTHING;
-- 重置序列值 -- 重置序列值
SELECT setval('users_id_seq', (SELECT COALESCE(MAX(id), 1) FROM users)); SELECT setval('sys_user_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_user));
SELECT setval('roles_id_seq', (SELECT COALESCE(MAX(id), 1) FROM roles)); 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_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_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)); 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), update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_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, FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
UNIQUE (role_id, permission_id) 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_username ON sys_user(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_users_email ON sys_user(email);
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status); CREATE INDEX IF NOT EXISTS idx_users_status ON sys_user(status);
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at); 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_role_key ON sys_role(role_key);
CREATE INDEX IF NOT EXISTS idx_roles_status ON roles(status); CREATE INDEX IF NOT EXISTS idx_roles_status ON sys_role(status);
CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON roles(deleted_at); 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); CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
@@ -1,7 +1,6 @@
-- Novalon管理系统审计日志表 -- Novalon管理系统审计日志表
-- 版本: V7 -- 版本: V7
-- 描述: 创建审计日志表,记录数据变更前后的完整对比 -- 描述: 创建审计日志表,记录数据变更前后的完整对比
CREATE TABLE IF NOT EXISTS audit_log ( CREATE TABLE IF NOT EXISTS audit_log (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL, entity_type VARCHAR(100) NOT NULL,
@@ -11,20 +10,22 @@ CREATE TABLE IF NOT EXISTS audit_log (
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
before_data JSONB, before_data JSONB,
after_data JSONB, after_data JSONB,
changed_fields TEXT[], changed_fields TEXT [],
ip_address VARCHAR(50), ip_address VARCHAR(50),
user_agent TEXT, user_agent TEXT,
description 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_type ON audit_log(entity_type);
CREATE INDEX idx_audit_log_entity_id ON audit_log(entity_id); 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_operation_type ON audit_log(operation_type);
CREATE INDEX idx_audit_log_operator ON audit_log(operator); 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_operation_time ON audit_log(operation_time);
CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id); CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id);
COMMENT ON TABLE audit_log IS '审计日志表'; COMMENT ON TABLE audit_log IS '审计日志表';
COMMENT ON COLUMN audit_log.id IS '主键ID'; COMMENT ON COLUMN audit_log.id IS '主键ID';
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)'; 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.before_data IS '变更前数据(JSON格式)';
COMMENT ON COLUMN audit_log.after_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.changed_fields IS '变更字段列表';
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址'; COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';COMMENT ON COLUMN audit_log.description IS '操作描述';
COMMENT ON COLUMN audit_log.user_agent IS '用户代理';
COMMENT ON COLUMN audit_log.description IS '操作描述';
COMMENT ON COLUMN audit_log.created_at 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; package cn.novalon.manage.sys.audit;
import cn.novalon.manage.sys.audit.domain.AuditLog; 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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
@@ -34,11 +34,11 @@ public class AuditLogAspect {
private static final Logger logger = LoggerFactory.getLogger(AuditLogAspect.class); private static final Logger logger = LoggerFactory.getLogger(AuditLogAspect.class);
private final IAuditLogRepository auditLogRepository; private final IAuditLogService auditLogService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public AuditLogAspect(IAuditLogRepository auditLogRepository, ObjectMapper objectMapper) { public AuditLogAspect(IAuditLogService auditLogService, ObjectMapper objectMapper) {
this.auditLogRepository = auditLogRepository; this.auditLogService = auditLogService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
} }
@@ -99,6 +99,9 @@ public class AuditLogAspect {
String finalOperationType = operationTypeHolder[0]; String finalOperationType = operationTypeHolder[0];
String finalBeforeData = beforeDataHolder[0]; String finalBeforeData = beforeDataHolder[0];
logger.debug("保存操作审计日志: entityType={}, entityIdHolder={}, extractedEntityId={}, finalEntityId={}",
entityType, entityIdHolder[0], extractEntityId(savedEntity), finalEntityId);
return createAndSaveAuditLog( return createAndSaveAuditLog(
entityType, finalEntityId, finalOperationType, entityType, finalEntityId, finalOperationType,
finalBeforeData, afterData, savedEntity finalBeforeData, afterData, savedEntity
@@ -163,18 +166,22 @@ public class AuditLogAspect {
private Mono<Void> createAndSaveAuditLog(String entityType, Long entityId, private Mono<Void> createAndSaveAuditLog(String entityType, Long entityId,
String operationType, String beforeData, String operationType, String beforeData,
String afterData, Object entity) { String afterData, Object entity) {
logger.debug("创建审计日志: entityType={}, entityId={}, operationType={}", entityType, entityId, operationType);
return ReactiveSecurityContextHolder.getContext() return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal()) .map(ctx -> ctx.getAuthentication().getPrincipal())
.defaultIfEmpty("system") .defaultIfEmpty("system")
.flatMap(principal -> { .flatMap(principal -> {
AuditLog auditLog = new AuditLog(); AuditLog auditLog = new AuditLog();
auditLog.setEntityType(entityType); auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId); auditLog.setEntityId(entityId != null ? entityId : 0L);
auditLog.setOperationType(operationType); auditLog.setOperationType(operationType);
auditLog.setOperator(principal instanceof String ? (String) principal : "system"); auditLog.setOperator(principal instanceof String ? (String) principal : "system");
auditLog.setBeforeData(beforeData); auditLog.setBeforeData(beforeData);
auditLog.setAfterData(afterData); auditLog.setAfterData(afterData);
logger.debug("审计日志对象: entityId={}, entityType={}, operationType={}",
auditLog.getEntityId(), auditLog.getEntityType(), auditLog.getOperationType());
if (beforeData != null && afterData != null) { if (beforeData != null && afterData != null) {
String[] changedFields = extractChangedFields(beforeData, afterData); String[] changedFields = extractChangedFields(beforeData, afterData);
auditLog.setChangedFields(changedFields); auditLog.setChangedFields(changedFields);
@@ -182,7 +189,7 @@ public class AuditLogAspect {
auditLog.setDescription(generateDescription(entityType, operationType, entityId)); auditLog.setDescription(generateDescription(entityType, operationType, entityId));
return auditLogRepository.save(auditLog) return auditLogService.save(auditLog)
.doOnSuccess(saved -> logger.debug("审计日志保存成功: {} - {}", .doOnSuccess(saved -> logger.debug("审计日志保存成功: {} - {}",
entityType, operationType)) entityType, operationType))
.doOnError(error -> logger.error("审计日志保存失败: {}", .doOnError(error -> logger.error("审计日志保存失败: {}",
@@ -231,11 +238,14 @@ public class AuditLogAspect {
} }
private Long extractEntityId(Object entity) { private Long extractEntityId(Object entity) {
logger.debug("提取实体ID: entity class={}", entity.getClass().getName());
if (entity instanceof Persistable) { if (entity instanceof Persistable) {
Persistable<?> persistable = (Persistable<?>) entity; Persistable<?> persistable = (Persistable<?>) entity;
Object id = persistable.getId(); Object id = persistable.getId();
logger.debug("Persistable实体ID: id={}, isNew={}", id, persistable.isNew());
return id != null ? ((Number) id).longValue() : null; return id != null ? ((Number) id).longValue() : null;
} }
logger.debug("实体不是Persistable类型");
return null; return null;
} }
@@ -1,9 +1,7 @@
package cn.novalon.manage.sys.audit.domain; package cn.novalon.manage.sys.audit.domain;
import cn.novalon.manage.sys.core.domain.BaseDomain;
import io.swagger.v3.oas.annotations.media.Schema; 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; import java.time.LocalDateTime;
@@ -13,73 +11,44 @@ import java.time.LocalDateTime;
* @author 张翔 * @author 张翔
* @date 2026-04-01 * @date 2026-04-01
*/ */
@Table("audit_log")
@Schema(description = "审计日志实体") @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") @Schema(description = "实体类型(如User, Role等)", example = "User")
private String entityType; private String entityType;
@Column("entity_id")
@Schema(description = "实体ID", example = "1") @Schema(description = "实体ID", example = "1")
private Long entityId; private Long entityId;
@Column("operation_type")
@Schema(description = "操作类型(CREATE, UPDATE, DELETE", example = "UPDATE") @Schema(description = "操作类型(CREATE, UPDATE, DELETE", example = "UPDATE")
private String operationType; private String operationType;
@Column("operator")
@Schema(description = "操作人", example = "admin") @Schema(description = "操作人", example = "admin")
private String operator; private String operator;
@Column("operation_time")
@Schema(description = "操作时间") @Schema(description = "操作时间")
private LocalDateTime operationTime; private LocalDateTime operationTime;
@Column("before_data")
@Schema(description = "变更前数据(JSON格式)") @Schema(description = "变更前数据(JSON格式)")
private String beforeData; private String beforeData;
@Column("after_data")
@Schema(description = "变更后数据(JSON格式)") @Schema(description = "变更后数据(JSON格式)")
private String afterData; private String afterData;
@Column("changed_fields")
@Schema(description = "变更字段列表") @Schema(description = "变更字段列表")
private String[] changedFields; private String[] changedFields;
@Column("ip_address")
@Schema(description = "IP地址", example = "192.168.1.100") @Schema(description = "IP地址", example = "192.168.1.100")
private String ipAddress; private String ipAddress;
@Column("user_agent")
@Schema(description = "用户代理") @Schema(description = "用户代理")
private String userAgent; private String userAgent;
@Column("description")
@Schema(description = "操作描述", example = "更新用户信息") @Schema(description = "操作描述", example = "更新用户信息")
private String description; private String description;
@Column("created_at")
@Schema(description = "记录创建时间")
private LocalDateTime createdAt;
public AuditLog() { public AuditLog() {
this.operationTime = LocalDateTime.now(); this.operationTime = LocalDateTime.now();
this.createdAt = LocalDateTime.now();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
} }
public String getEntityType() { public String getEntityType() {
@@ -169,12 +138,4 @@ public class AuditLog {
public void setDescription(String description) { public void setDescription(String description) {
this.description = 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; package cn.novalon.manage.sys.audit.repository;
import cn.novalon.manage.sys.audit.domain.AuditLog; 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.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -14,8 +12,15 @@ import java.time.LocalDateTime;
* @author 张翔 * @author 张翔
* @date 2026-04-01 * @date 2026-04-01
*/ */
@Repository public interface IAuditLogRepository {
public interface IAuditLogRepository extends R2dbcRepository<AuditLog, Long> {
Mono<AuditLog> findById(Long id);
Mono<AuditLog> save(AuditLog auditLog);
Mono<Void> deleteById(Long id);
Flux<AuditLog> findAll();
Flux<AuditLog> findByEntityType(String entityType); 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; package cn.novalon.manage.sys.core.domain;
import cn.novalon.manage.common.util.SnowflakeId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
@@ -64,4 +65,14 @@ public abstract class BaseDomain {
public void setDeletedAt(LocalDateTime deletedAt) { public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = 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; 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; 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) { public void setPermissionId(Long permissionId) {
this.permissionId = 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; 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 @Override
public Mono<SysRole> createRole(CreateRoleCommand command) { public Mono<SysRole> createRole(CreateRoleCommand command) {
SysRole role = new SysRole(); SysRole role = new SysRole();
role.generateId();
role.setRoleName(command.roleName()); role.setRoleName(command.roleName());
role.setRoleKey(command.roleKey()); role.setRoleKey(command.roleKey());
role.setRoleSort(command.roleSort()); role.setRoleSort(command.roleSort());
@@ -98,6 +98,7 @@ public class SysUserService implements ISysUserService {
logger.info("SysUserService.createUser - 用户名: {}, 密码前缀: {}", logger.info("SysUserService.createUser - 用户名: {}, 密码前缀: {}",
user.getUsername(), user.getUsername(),
user.getPassword() != null ? user.getPassword().substring(0, 7) : "null"); user.getPassword() != null ? user.getPassword().substring(0, 7) : "null");
user.generateId();
if (user.getPassword() != null && !user.getPassword().startsWith("$2a$") if (user.getPassword() != null && !user.getPassword().startsWith("$2a$")
&& !user.getPassword().startsWith("$2b$")) { && !user.getPassword().startsWith("$2b$")) {
logger.info("密码不以$2a$或$2b$开头,重新编码"); logger.info("密码不以$2a$或$2b$开头,重新编码");
@@ -106,7 +107,6 @@ public class SysUserService implements ISysUserService {
} else { } else {
logger.info("密码已编码,跳过重新编码"); logger.info("密码已编码,跳过重新编码");
} }
user.setCreatedAt(LocalDateTime.now());
if (user.getStatus() == null) { if (user.getStatus() == null) {
user.setStatus(StatusConstants.ENABLED); user.setStatus(StatusConstants.ENABLED);
} }
@@ -116,6 +116,7 @@ public class SysUserService implements ISysUserService {
@Override @Override
public Mono<SysUser> createUser(CreateUserCommand command) { public Mono<SysUser> createUser(CreateUserCommand command) {
SysUser user = new SysUser(); SysUser user = new SysUser();
user.generateId();
user.setUsername(command.username().getValue()); user.setUsername(command.username().getValue());
user.setPassword(passwordEncoder.encode(command.password().getValue())); user.setPassword(passwordEncoder.encode(command.password().getValue()));
user.setEmail(command.email().getValue()); user.setEmail(command.email().getValue());
@@ -123,7 +124,6 @@ public class SysUserService implements ISysUserService {
user.setPhone(command.phone()); user.setPhone(command.phone());
user.setRoleId(command.roleId()); user.setRoleId(command.roleId());
user.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED); user.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
user.setCreatedAt(LocalDateTime.now());
return userRepository.save(user); return userRepository.save(user);
} }
@@ -267,8 +267,7 @@ public class SysUserService implements ISysUserService {
.doOnSuccess(v -> logger.debug("成功保存用户角色关联")) .doOnSuccess(v -> logger.debug("成功保存用户角色关联"))
.doOnError(e -> logger.error("保存用户角色关联失败", e)); .doOnError(e -> logger.error("保存用户角色关联失败", e));
}) })
.then() .then());
);
} }
@Override @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.Pattern;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.util.List;
/** /**
* 用户注册请求DTO * 用户注册请求DTO
* *
@@ -42,6 +44,9 @@ public class UserRegisterRequest {
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone; private String phone;
@Schema(description = "角色ID列表", example = "[1, 2]")
private List<Long> roles;
public String getUsername() { public String getUsername() {
return username; return username;
} }
@@ -81,4 +86,12 @@ public class UserRegisterRequest {
public void setPhone(String phone) { public void setPhone(String phone) {
this.phone = 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.domain.SysUser;
import cn.novalon.manage.sys.core.service.ISysUserService; import cn.novalon.manage.sys.core.service.ISysUserService;
import cn.novalon.manage.common.dto.PageRequest; 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.PasswordChangeRequest;
import cn.novalon.manage.sys.dto.request.UserRegisterRequest; import cn.novalon.manage.sys.dto.request.UserRegisterRequest;
import cn.novalon.manage.sys.dto.request.UserUpdateRequest; import cn.novalon.manage.sys.dto.request.UserUpdateRequest;
@@ -135,6 +136,14 @@ public class SysUserHandler {
null null
)) ))
.flatMap(userService::createUser) .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)); .flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
}); });
} }
@@ -249,9 +258,8 @@ public class SysUserHandler {
@OperationLog(operation = "分配角色", module = "用户管理") @OperationLog(operation = "分配角色", module = "用户管理")
public Mono<ServerResponse> assignRoles(ServerRequest request) { public Mono<ServerResponse> assignRoles(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id")); Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(new org.springframework.core.ParameterizedTypeReference<List<Long>>() { return request.bodyToMono(AssignRolesRequest.class)
}) .flatMap(req -> userService.assignRolesToUser(id, req.getRoleIds()))
.flatMap(roleIds -> userService.assignRolesToUser(id, roleIds))
.then(ServerResponse.ok().build()) .then(ServerResponse.ok().build())
.onErrorResume(error -> { .onErrorResume(error -> {
logger.error("分配角色失败", error); logger.error("分配角色失败", error);
+3
View File
@@ -34,3 +34,6 @@ TEST_WORKERS=4
# 测试报告配置(可选) # 测试报告配置(可选)
TEST_REPORT_FOLDER=playwright-report TEST_REPORT_FOLDER=playwright-report
TEST_RESULTS_FOLDER=test-results TEST_RESULTS_FOLDER=test-results
# API签名密钥配置
VITE_SIGNATURE_SECRET=your-secret-key-here
+3
View File
@@ -2,7 +2,10 @@ node_modules
dist dist
.DS_Store .DS_Store
*.log *.log
.env
.env.local .env.local
.env.*.local .env.*.local
coverage coverage
.nyc_output .nyc_output
debug-*.png
e2e/debug/
+21 -5
View File
@@ -1,18 +1,34 @@
FROM node:18-alpine AS builder # 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
COPY package*.json ./ # 安装 pnpm
RUN npm ci RUN npm install -g pnpm@8.15.0
# 复制 package.json 和 lock 文件
COPY package.json pnpm-lock.yaml ./
# 安装依赖
RUN pnpm install
# 复制源代码
COPY . . COPY . .
RUN npm run build
# 构建生产版本
RUN pnpm run build:prod
# 生产阶段
FROM nginx:alpine FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html # 复制自定义 nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 暴露端口
EXPOSE 80 EXPOSE 80
# 启动 nginx
CMD ["nginx", "-g", "daemon off;"] 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 { private calculateStats(result: FullResult): TestStats {
const suites = result.suites || []; const allTests = this.testResults;
const allTests = suites.flatMap(suite =>
suite.specs.flatMap(spec => spec.tests) 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 passed = allTests.filter(t => t.status === 'passed');
const failed = allTests.filter(t => t.status === 'failed'); const failed = allTests.filter(t => t.status === 'failed');
const skipped = allTests.filter(t => t.status === 'skipped'); const skipped = allTests.filter(t => t.status === 'skipped');
const flaky = allTests.filter(t => t.status === 'passed' && t.retry >= 1); 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 avgDuration = totalDuration / allTests.length;
const passRate = (passed.length / allTests.length) * 100; const passRate = (passed.length / allTests.length) * 100;
@@ -67,8 +82,8 @@ class CustomReporter implements Reporter {
totalDuration, totalDuration,
avgDuration, avgDuration,
slowestTests: allTests slowestTests: allTests
.filter(t => t.duration) .filter(t => t.duration > 0)
.sort((a, b) => (b.duration || 0) - (a.duration || 0)) .sort((a, b) => b.duration - a.duration)
.slice(0, 10), .slice(0, 10),
failedTests: failed, failedTests: failed,
}; };
@@ -98,9 +113,13 @@ class CustomReporter implements Reporter {
if (stats.failedTests.length > 0) { if (stats.failedTests.length > 0) {
console.log('❌ 失败的测试:'); console.log('❌ 失败的测试:');
stats.failedTests.forEach((test, index) => { stats.failedTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.title}`); console.log(` ${index + 1}. ${test.title || '未命名测试'}`);
console.log(` 位置: ${test.location.file}:${test.location.line}`); if (test.location?.file) {
console.log(` 错误: ${test.error?.message}`); console.log(` 位置: ${test.location.file}:${test.location.line || 0}`);
}
if (test.error?.message) {
console.log(` 错误: ${test.error.message}`);
}
}); });
console.log(''); 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 });
});
});
+301 -11
View File
@@ -8,8 +8,21 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
let backendProcess: ChildProcess | null = null; let backendProcess: ChildProcess | null = null;
let gatewayProcess: ChildProcess | null = null;
let healthCheckInterval: NodeJS.Timeout | 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> { async function checkBackendHealth(): Promise<boolean> {
try { try {
const response = await fetch('http://localhost:8084/actuator/health', { 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() { function startHealthMonitoring() {
if (healthCheckInterval) { if (healthCheckInterval) {
clearInterval(healthCheckInterval); clearInterval(healthCheckInterval);
} }
healthCheckInterval = setInterval(async () => { healthCheckInterval = setInterval(async () => {
const isHealthy = await checkBackendHealth(); const backendHealthy = await checkBackendHealth();
if (!isHealthy) { const gatewayHealthy = await checkGatewayHealth();
const frontendHealthy = await checkFrontendHealth();
if (!backendHealthy) {
console.error('⚠️ 后端服务健康检查失败!'); console.error('⚠️ 后端服务健康检查失败!');
} }
if (!gatewayHealthy) {
console.error('⚠️ 网关服务健康检查失败!');
}
if (!frontendHealthy) {
console.error('⚠️ 前端服务健康检查失败!');
}
}, 30000); }, 30000);
} }
@@ -51,6 +99,10 @@ async function globalSetup(config: FullConfig) {
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
process.env.PLAYWRIGHT_HEADLESS = 'false'; process.env.PLAYWRIGHT_HEADLESS = 'false';
const backendAlreadyRunning = await checkBackendHealth();
if (backendAlreadyRunning) {
console.log('✅ 后端服务已在运行,跳过启动');
} else {
const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app'); const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app');
const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar'); const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar');
@@ -116,6 +168,81 @@ async function globalSetup(config: FullConfig) {
console.log('⏳ 等待后端服务就绪...'); console.log('⏳ 等待后端服务就绪...');
await waitForBackendReady(); 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('🧹 清理测试数据...'); console.log('🧹 清理测试数据...');
await cleanupTestData(); await cleanupTestData();
@@ -125,18 +252,81 @@ async function globalSetup(config: FullConfig) {
console.log('✅ 全局测试环境设置完成'); 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> { async function waitForBackendReady(): Promise<void> {
const maxRetries = 60; const maxRetries = 90;
const retryInterval = 1000; const retryInterval = 1000;
for (let i = 0; i < maxRetries; i++) { for (let i = 0; i < maxRetries; i++) {
renderProgressBar('⏳ 后端服务启动中', i, maxRetries);
try { 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) { if (response.ok) {
const data = await response.json(); const data = await response.json();
if (data.status === 'UP') { if (data.status === 'UP') {
process.stdout.write('\r' + ' '.repeat(80) + '\r');
console.log(`✅ 后端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`); console.log(`✅ 后端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
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; return;
} else {
console.log(`⚠️ 后端服务连通性验证失败,状态码: ${loginTest.status}`);
}
} catch (error) {
console.log('⚠️ 后端服务连通性验证失败,继续等待...');
}
} }
} }
} catch (error) { } catch (error) {
@@ -151,17 +341,93 @@ async function waitForBackendReady(): Promise<void> {
throw new Error('❌ 后端服务启动超时'); 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> { async function cleanupTestData(): Promise<void> {
try { try {
// 登录获取token // 登录获取token(通过网关)
const loginResponse = await fetch('http://localhost:8084/api/auth/login', { const loginResponse = await fetch('http://localhost:8080/api/auth/login', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
username: 'admin', username: 'admin',
password: 'admin123' password: 'Test@123'
}) })
}); });
@@ -174,7 +440,7 @@ async function cleanupTestData(): Promise<void> {
const token = loginData.token; const token = loginData.token;
// 获取所有用户 // 获取所有用户
const usersResponse = await fetch('http://localhost:8084/api/users', { const usersResponse = await fetch('http://localhost:8080/api/users', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@@ -187,7 +453,7 @@ async function cleanupTestData(): Promise<void> {
for (const user of users) { for (const user of users) {
if (user.id > 10) { if (user.id > 10) {
try { try {
await fetch(`http://localhost:8084/api/users/${user.id}`, { await fetch(`http://localhost:8080/api/users/${user.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
@@ -202,7 +468,7 @@ async function cleanupTestData(): Promise<void> {
} }
// 获取所有角色 // 获取所有角色
const rolesResponse = await fetch('http://localhost:8084/api/roles', { const rolesResponse = await fetch('http://localhost:8080/api/roles', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@@ -215,7 +481,7 @@ async function cleanupTestData(): Promise<void> {
for (const role of roles) { for (const role of roles) {
if (role.id > 4) { if (role.id > 4) {
try { try {
await fetch(`http://localhost:8084/api/roles/${role.id}`, { await fetch(`http://localhost:8080/api/roles/${role.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
@@ -265,6 +531,30 @@ 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('✅ 全局测试环境清理完成'); 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 { export class DictionaryManagementPage {
readonly page: Page; readonly page: Page;
readonly table: Locator; readonly table: Locator;
readonly createDictTypeButton: Locator; readonly createDictButton: Locator;
readonly createDictDataButton: Locator; readonly saveButton: Locator;
readonly searchInput: Locator; readonly dialog: Locator;
readonly searchButton: Locator; readonly dictNameInput: Locator;
readonly successMessage: Locator; readonly dictTypeInput: Locator;
readonly dictTypeTable: Locator; readonly statusSelect: Locator;
readonly dictDataTable: Locator; readonly remarkInput: Locator;
constructor(page: Page) { constructor(page: Page) {
this.page = page; this.page = page;
this.table = page.locator('.el-table').or(page.locator('.dict-table')); this.table = page.locator('.el-table');
this.createDictTypeButton = page.getByRole('button', { name: '新增字典类型' }).or(page.locator('button:has-text("新增字典类型")')); this.createDictButton = page.getByRole('button', { name: '新增字典' });
this.createDictDataButton = page.getByRole('button', { name: '新增字典数据' }).or(page.locator('button:has-text("新增字典数据")')); this.saveButton = page.getByRole('button', { name: '确定' });
this.searchInput = page.locator('input[placeholder*="搜索"]').or(page.locator('input[name*="keyword"]')); this.dialog = page.locator('.el-dialog');
this.searchButton = page.getByRole('button', { name: '搜索' }).or(page.locator('button:has-text("搜索")')); this.dictNameInput = page.locator('.el-dialog').getByRole('textbox', { name: '字典名称' });
this.successMessage = page.locator('.el-message--success').or(page.locator('.success-message')); this.dictTypeInput = page.locator('.el-dialog').getByRole('textbox', { name: '字典类型' });
this.dictTypeTable = page.locator('.dict-type-table').or(page.locator('.el-table').first()); this.statusSelect = page.locator('.el-dialog').getByRole('combobox', { name: '状态' });
this.dictDataTable = page.locator('.dict-data-table').or(page.locator('.el-table').nth(1)); this.remarkInput = page.locator('.el-dialog').getByRole('textbox', { name: '备注' });
} }
async goto() { async goto() {
@@ -40,156 +40,57 @@ export class DictionaryManagementPage {
} }
} }
async clickCreateDictType() { async createDict(dictName: string, dictType: string, status: string = '0', remark?: string) {
await this.createDictTypeButton.click(); await this.createDictButton.click();
await this.page.waitForTimeout(500); await this.page.waitForTimeout(500);
}
async clickCreateDictData() { await this.dictNameInput.fill(dictName);
await this.createDictDataButton.click(); await this.dictTypeInput.fill(dictType);
await this.page.waitForTimeout(500);
}
async fillDictTypeForm(dictTypeData: { if (status) {
dictName: string; await this.statusSelect.click();
dictType: string;
status?: string;
remark?: string;
}) {
const dialog = this.page.locator('.el-dialog');
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);
}
}
}
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.waitForTimeout(300);
await this.page.getByRole('option', { name: dictData.dictType }).click(); await this.page.getByRole('option', { name: status === '0' ? '正常' : '停用' }).click();
}
} }
if (dictData.cssClass) { if (remark) {
const cssClassInput = dialog.locator('input[placeholder*="CSS"]'); await this.remarkInput.fill(remark);
if (await cssClassInput.count() > 0) {
await cssClassInput.fill(dictData.cssClass);
}
} }
if (dictData.listClass) { await this.saveButton.click();
const listClassInput = dialog.locator('input[placeholder*="列表"]'); await this.page.waitForLoadState('networkidle');
if (await listClassInput.count() > 0) {
await listClassInput.fill(dictData.listClass);
}
} }
if (dictData.isDefault) { async editDict(dictName: string, newDictName: string) {
const defaultRadio = dialog.locator(`input[value="${dictData.isDefault}"]`); const row = this.table.locator('tr').filter({ hasText: dictName }).first();
if (await defaultRadio.count() > 0) { const editBtn = row.getByRole('button', { name: '编辑' });
await defaultRadio.check(); 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');
} }
if (dictData.status) { async deleteDict(dictName: string) {
const statusRadio = dialog.locator(`input[value="${dictData.status}"]`); const row = this.table.locator('tr').filter({ hasText: dictName }).first();
if (await statusRadio.count() > 0) { const deleteBtn = row.getByRole('button', { name: '删除' });
await statusRadio.check(); 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');
} }
if (dictData.sort !== undefined) { async getDictCount() {
const sortInput = dialog.locator('input[type="number"]'); const rows = await this.table.locator('.el-table__row').count();
if (await sortInput.count() > 0) { return rows;
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 containsText(text: string): Promise<boolean> { async containsText(text: string): Promise<boolean> {
return await this.table.getByText(text).count() > 0; 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,40 +4,47 @@ export class NotificationPage {
readonly page: Page; readonly page: Page;
readonly table; readonly table;
readonly addButton; readonly addButton;
readonly editButton;
readonly deleteButton;
readonly saveButton; readonly saveButton;
readonly cancelButton; readonly cancelButton;
readonly searchInput; readonly dialog;
readonly searchButton;
readonly titleInput; readonly titleInput;
readonly contentInput; readonly contentInput;
readonly typeSelect; readonly noticeTypeSelect;
readonly statusSelect; readonly statusSelect;
constructor(page: Page) { constructor(page: Page) {
this.page = page; this.page = page;
this.table = page.locator('.el-table'); this.table = page.locator('.el-table');
this.addButton = page.getByRole('button', { name: '新增' }); 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.saveButton = page.getByRole('button', { name: '确定' });
this.cancelButton = page.getByRole('button', { name: '取消' }); this.cancelButton = page.getByRole('button', { name: '取消' });
this.searchInput = page.getByPlaceholder('搜索通知标题'); this.dialog = page.locator('.el-dialog');
this.searchButton = page.getByRole('button', { name: '搜索' }); this.titleInput = page.locator('.el-dialog').getByRole('textbox', { name: '公告标题' });
this.titleInput = page.getByPlaceholder('请输入通知标题'); this.contentInput = page.locator('.el-dialog').getByRole('textbox', { name: '公告内容' });
this.contentInput = page.getByPlaceholder('请输入通知内容'); this.noticeTypeSelect = page.locator('.el-dialog').getByRole('combobox', { name: '公告类型' });
this.typeSelect = page.locator('.el-select'); this.statusSelect = page.locator('.el-dialog').getByRole('combobox', { name: '状态' });
this.statusSelect = page.locator('.el-select');
} }
async goto() { async goto() {
await this.page.goto('/system/notice'); try {
console.log('导航到通知管理页面...');
await this.page.goto('/notice');
await this.page.waitForLoadState('networkidle'); 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.addButton.click();
await this.page.waitForTimeout(500);
await this.titleInput.fill(title); await this.titleInput.fill(title);
await this.contentInput.fill(content); await this.contentInput.fill(content);
@@ -48,7 +55,9 @@ export class NotificationPage {
async editNotification(title: string, newContent: string) { async editNotification(title: string, newContent: string) {
const row = this.table.locator('tr').filter({ hasText: title }).first(); 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.clear();
await this.contentInput.fill(newContent); await this.contentInput.fill(newContent);
@@ -59,34 +68,21 @@ export class NotificationPage {
async deleteNotification(title: string) { async deleteNotification(title: string) {
const row = this.table.locator('tr').filter({ hasText: title }).first(); const row = this.table.locator('tr').filter({ hasText: title }).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'); 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() { async getTableRowCount() {
const rows = await this.table.locator('.el-table__row').count(); const rows = await this.table.locator('.el-table__row').count();
return rows; return rows;
} }
async verifyTableContains(text: string) {
await expect(this.table).toContainText(text);
}
} }
@@ -4,31 +4,23 @@ export class SystemConfigPage {
readonly page: Page; readonly page: Page;
readonly table; readonly table;
readonly addButton; readonly addButton;
readonly editButton;
readonly deleteButton;
readonly saveButton; readonly saveButton;
readonly cancelButton; readonly cancelButton;
readonly searchInput; readonly dialog;
readonly searchButton;
readonly configNameInput; readonly configNameInput;
readonly configKeyInput; readonly configKeyInput;
readonly configValueInput; readonly configValueInput;
readonly configTypeSelect;
constructor(page: Page) { constructor(page: Page) {
this.page = page; this.page = page;
this.table = page.locator('.el-table'); this.table = page.locator('.el-table');
this.addButton = page.getByRole('button', { name: '新增配置' }); 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.saveButton = page.getByRole('button', { name: '确定' });
this.cancelButton = page.getByRole('button', { name: '取消' }); this.cancelButton = page.getByRole('button', { name: '取消' });
this.searchInput = page.getByPlaceholder('搜索配置名称'); this.dialog = page.locator('.el-dialog');
this.searchButton = page.getByRole('button', { name: '搜索' }); this.configNameInput = page.locator('.el-dialog').getByRole('textbox', { name: '参数名称' });
this.configNameInput = page.getByPlaceholder('请输入配置名称'); this.configKeyInput = page.locator('.el-dialog').getByRole('textbox', { name: '参数键名' });
this.configKeyInput = page.getByPlaceholder('请输入配置键名'); this.configValueInput = page.locator('.el-dialog').getByRole('textbox', { name: '参数值' });
this.configValueInput = page.getByPlaceholder('请输入配置键值');
this.configTypeSelect = page.locator('.el-select');
} }
async goto() { 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.addButton.click();
await this.page.waitForTimeout(500);
await this.configNameInput.fill(configName); await this.configNameInput.fill(configName);
await this.configKeyInput.fill(configKey); await this.configKeyInput.fill(configKey);
@@ -61,7 +54,9 @@ export class SystemConfigPage {
async editConfig(configKey: string, newValue: string) { async editConfig(configKey: string, newValue: string) {
const row = this.table.locator('tr').filter({ hasText: configKey }).first(); 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.clear();
await this.configValueInput.fill(newValue); await this.configValueInput.fill(newValue);
@@ -72,34 +67,21 @@ export class SystemConfigPage {
async deleteConfig(configKey: string) { async deleteConfig(configKey: string) {
const row = this.table.locator('tr').filter({ hasText: configKey }).first(); 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'); 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() { async getTableRowCount() {
const rows = await this.table.locator('.el-table__row').count(); const rows = await this.table.locator('.el-table__row').count();
return rows; 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