develop #2
Vendored
+310
@@ -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 '⚠️ 流水线执行不稳定!'
|
||||
// 可以添加不稳定状态通知
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:Commit**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:Commit**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:Commit**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:Commit**
|
||||
|
||||
```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'`
|
||||
|
||||
- [ ] **步骤 4:Commit**
|
||||
|
||||
```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. 优先使用单元测试覆盖功能细节
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:Commit**
|
||||
|
||||
```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 Store(1-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)
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
package cn.novalon.manage.app.config;
|
||||
|
||||
import io.r2dbc.spi.ConnectionFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
|
||||
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
|
||||
|
||||
/**
|
||||
* R2DBC数据库初始化配置
|
||||
*
|
||||
* 用于测试环境的H2数据库初始化
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-04-03
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(name = "spring.profiles.active", havingValue = "test")
|
||||
public class R2dbcInitConfig {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(R2dbcInitConfig.class);
|
||||
|
||||
@Bean
|
||||
public ConnectionFactoryInitializer connectionFactoryInitializer(ConnectionFactory connectionFactory) {
|
||||
logger.info("Initializing R2DBC database with H2 schema and data");
|
||||
|
||||
ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
|
||||
initializer.setConnectionFactory(connectionFactory);
|
||||
|
||||
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
|
||||
populator.addScript(new ClassPathResource("schema-h2.sql"));
|
||||
populator.addScript(new ClassPathResource("data-h2.sql"));
|
||||
|
||||
initializer.setDatabasePopulator(populator);
|
||||
|
||||
return initializer;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ spring:
|
||||
password: novalon123
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
validate-on-migrate: true
|
||||
|
||||
rate:
|
||||
limit:
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# H2数据库配置(用于测试环境)
|
||||
|
||||
spring:
|
||||
r2dbc:
|
||||
url: r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
username: sa
|
||||
password:
|
||||
pool:
|
||||
initial-size: 5
|
||||
max-size: 20
|
||||
max-idle-time: 30m
|
||||
max-life-time: 1h
|
||||
acquire-timeout: 5s
|
||||
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
username: sa
|
||||
password:
|
||||
driver-class-name: org.h2.Driver
|
||||
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
settings:
|
||||
web-allow-others: true
|
||||
|
||||
flyway:
|
||||
enabled: false
|
||||
|
||||
sql:
|
||||
init:
|
||||
mode: always
|
||||
continue-on-error: false
|
||||
schema-locations: classpath:schema-h2.sql
|
||||
data-locations: classpath:data-h2.sql
|
||||
|
||||
# 测试专用配置
|
||||
test:
|
||||
database:
|
||||
type: h2
|
||||
in-memory: true
|
||||
cleanup:
|
||||
enabled: true
|
||||
strategy: truncate
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
cn.novalon.manage: DEBUG
|
||||
org.springframework.r2dbc: DEBUG
|
||||
org.springframework.jdbc: DEBUG
|
||||
org.flywaydb: INFO
|
||||
com.h2database: WARN
|
||||
@@ -5,26 +5,23 @@ spring:
|
||||
application:
|
||||
name: manage-app
|
||||
r2dbc:
|
||||
url: r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
username: sa
|
||||
password:
|
||||
url: r2dbc:postgresql://localhost:55432/manage_system
|
||||
username: novalon
|
||||
password: novalon123
|
||||
pool:
|
||||
initial-size: 5
|
||||
max-size: 20
|
||||
max-idle-time: 30m
|
||||
max-life-time: 1h
|
||||
acquire-timeout: 5s
|
||||
datasource:
|
||||
url: jdbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
username: sa
|
||||
password:
|
||||
driver-class-name: org.h2.Driver
|
||||
flyway:
|
||||
enabled: false
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
validate-on-migrate: true
|
||||
sql:
|
||||
init:
|
||||
mode: never
|
||||
security:
|
||||
user:
|
||||
name: disabled
|
||||
|
||||
+4
-2
@@ -2,7 +2,8 @@
|
||||
-- 用于测试环境
|
||||
|
||||
-- 插入测试角色
|
||||
INSERT INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by)
|
||||
MERGE INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by)
|
||||
KEY(id)
|
||||
VALUES
|
||||
(1, '超级管理员', 'admin', 1, 1, 'system', 'system'),
|
||||
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system'),
|
||||
@@ -11,7 +12,8 @@ VALUES
|
||||
|
||||
-- 插入测试用户
|
||||
-- BCrypt哈希值对应明文密码: Test@123
|
||||
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
|
||||
MERGE INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
|
||||
KEY(id)
|
||||
VALUES
|
||||
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'),
|
||||
(2, 'testadmin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'testadmin@novalon.com', '13800138001', '测试管理员', 1, 'system', 'system'),
|
||||
@@ -1,253 +0,0 @@
|
||||
-- H2 Database Schema for Integration Testing
|
||||
-- Create user table
|
||||
CREATE TABLE IF NOT EXISTS sys_user (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100),
|
||||
phone VARCHAR(20),
|
||||
nickname VARCHAR(100),
|
||||
role_id BIGINT,
|
||||
status INTEGER DEFAULT 1,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create role table
|
||||
CREATE TABLE IF NOT EXISTS sys_role (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
role_name VARCHAR(100) NOT NULL,
|
||||
role_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
role_sort INTEGER DEFAULT 0,
|
||||
status INTEGER DEFAULT 1,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create user role relation table
|
||||
CREATE TABLE IF NOT EXISTS user_role (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50),
|
||||
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
|
||||
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
|
||||
);
|
||||
|
||||
-- Create menu table
|
||||
CREATE TABLE IF NOT EXISTS sys_menu (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
menu_name VARCHAR(50) NOT NULL,
|
||||
parent_id BIGINT DEFAULT 0,
|
||||
order_num INTEGER DEFAULT 0,
|
||||
path VARCHAR(200),
|
||||
component VARCHAR(200),
|
||||
menu_type VARCHAR(1) DEFAULT 'C',
|
||||
visible VARCHAR(1) DEFAULT '1',
|
||||
status VARCHAR(1) DEFAULT '1',
|
||||
perms VARCHAR(100),
|
||||
icon VARCHAR(100),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create permission table
|
||||
CREATE TABLE IF NOT EXISTS sys_permission (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
permission_name VARCHAR(100) NOT NULL,
|
||||
permission_code VARCHAR(100) NOT NULL UNIQUE,
|
||||
resource VARCHAR(200),
|
||||
action VARCHAR(20),
|
||||
description VARCHAR(500),
|
||||
status INTEGER DEFAULT 1,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create role permission relation table
|
||||
CREATE TABLE IF NOT EXISTS sys_role_permission (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
role_id BIGINT NOT NULL,
|
||||
permission_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50),
|
||||
updated_by VARCHAR(50),
|
||||
CONSTRAINT fk_role_permission_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_role_permission_permission FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
|
||||
CONSTRAINT uk_role_permission UNIQUE (role_id, permission_id)
|
||||
);
|
||||
|
||||
-- Create dict type table
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_type (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
dict_name VARCHAR(100) NOT NULL,
|
||||
dict_type VARCHAR(100) NOT NULL UNIQUE,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
remark VARCHAR(500),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create dict data table
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_data (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
dict_sort INTEGER DEFAULT 0,
|
||||
dict_label VARCHAR(100) NOT NULL,
|
||||
dict_value VARCHAR(100) NOT NULL,
|
||||
dict_type VARCHAR(100) NOT NULL,
|
||||
css_class VARCHAR(100),
|
||||
list_class VARCHAR(100),
|
||||
is_default VARCHAR(1) DEFAULT 'N',
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create dictionary table (general)
|
||||
CREATE TABLE IF NOT EXISTS sys_dictionary (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
type VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
dict_value VARCHAR(500),
|
||||
remark VARCHAR(500),
|
||||
sort INTEGER DEFAULT 0,
|
||||
create_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create system config table
|
||||
CREATE TABLE IF NOT EXISTS sys_config (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
config_name VARCHAR(100) NOT NULL,
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
config_value VARCHAR(500) NOT NULL,
|
||||
config_type VARCHAR(1) DEFAULT 'N',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create login log table
|
||||
CREATE TABLE IF NOT EXISTS sys_login_log (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
ip VARCHAR(50),
|
||||
location VARCHAR(255),
|
||||
browser VARCHAR(50),
|
||||
os VARCHAR(50),
|
||||
status VARCHAR(1),
|
||||
message VARCHAR(255),
|
||||
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create exception log table
|
||||
CREATE TABLE IF NOT EXISTS sys_exception_log (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
title VARCHAR(100),
|
||||
exception_name VARCHAR(100),
|
||||
method_name VARCHAR(255),
|
||||
method_params TEXT,
|
||||
exception_msg TEXT,
|
||||
exception_stack TEXT,
|
||||
ip VARCHAR(50),
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create operation log table
|
||||
CREATE TABLE IF NOT EXISTS operation_log (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
operation VARCHAR(100),
|
||||
method VARCHAR(200),
|
||||
params TEXT,
|
||||
result TEXT,
|
||||
ip VARCHAR(50),
|
||||
duration BIGINT,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
error_msg TEXT,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create system notice table
|
||||
CREATE TABLE IF NOT EXISTS sys_notice (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
notice_title VARCHAR(50) NOT NULL,
|
||||
notice_type VARCHAR(1) NOT NULL,
|
||||
notice_content TEXT,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create user message table
|
||||
CREATE TABLE IF NOT EXISTS sys_user_message (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
notice_id BIGINT,
|
||||
message_title VARCHAR(255),
|
||||
message_content TEXT,
|
||||
is_read VARCHAR(1) DEFAULT '0',
|
||||
read_time TIMESTAMP,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create file management table
|
||||
CREATE TABLE IF NOT EXISTS sys_file (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
file_size BIGINT,
|
||||
file_type VARCHAR(100),
|
||||
file_extension VARCHAR(10),
|
||||
storage_type VARCHAR(50),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dict_type ON sys_dict_data(dict_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_login_log_username ON sys_login_log(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_log_username ON operation_log(username);
|
||||
-30
@@ -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
-4
@@ -1,6 +1,5 @@
|
||||
package cn.novalon.manage.app.integration;
|
||||
|
||||
import cn.novalon.manage.app.config.TestDatabaseConfig;
|
||||
import cn.novalon.manage.common.util.StatusConstants;
|
||||
import cn.novalon.manage.sys.core.domain.SysUser;
|
||||
import cn.novalon.manage.sys.core.domain.SysRole;
|
||||
@@ -14,7 +13,6 @@ import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
@@ -27,7 +25,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
/**
|
||||
* 用户服务集成测试
|
||||
*
|
||||
* 使用H2内存数据库进行集成测试
|
||||
* 使用PostgreSQL数据库进行集成测试
|
||||
*
|
||||
* 注意:此测试需要完整的Spring上下文,暂时禁用。
|
||||
* TODO: 优化集成测试配置
|
||||
@@ -38,7 +36,6 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
@Disabled("暂时禁用:集成测试配置需要优化")
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Import(TestDatabaseConfig.class)
|
||||
class SysUserServiceIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
spring:
|
||||
r2dbc:
|
||||
url: r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
username: sa
|
||||
password:
|
||||
url: r2dbc:postgresql://localhost:55432/manage_system
|
||||
username: novalon
|
||||
password: novalon123
|
||||
pool:
|
||||
enabled: true
|
||||
initial-size: 2
|
||||
max-size: 10
|
||||
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
validate-on-migrate: true
|
||||
|
||||
sql:
|
||||
init:
|
||||
mode: always
|
||||
continue-on-error: false
|
||||
schema-locations: classpath:schema-h2.sql
|
||||
data-locations: classpath:data-h2.sql
|
||||
|
||||
flyway:
|
||||
enabled: false
|
||||
mode: never
|
||||
|
||||
security:
|
||||
enabled: false
|
||||
|
||||
@@ -45,3 +45,32 @@ CREATE TABLE IF NOT EXISTS user_role (
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
|
||||
|
||||
-- 创建审计日志表
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
entity_type VARCHAR(100) NOT NULL,
|
||||
entity_id BIGINT,
|
||||
operation_type VARCHAR(20) NOT NULL,
|
||||
operator VARCHAR(100),
|
||||
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
before_data CLOB,
|
||||
after_data CLOB,
|
||||
changed_fields CLOB,
|
||||
ip_address VARCHAR(50),
|
||||
user_agent CLOB,
|
||||
description CLOB,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 创建审计日志索引
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_type ON audit_log(entity_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_id ON audit_log(entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_type ON audit_log(operation_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_operator ON audit_log(operator);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_time ON audit_log(operation_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);
|
||||
|
||||
@@ -60,12 +60,12 @@
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-h2</artifactId>
|
||||
<scope>test</scope>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
|
||||
+87
@@ -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();
|
||||
}
|
||||
+135
@@ -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;
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -1,6 +1,11 @@
|
||||
package cn.novalon.manage.db.entity;
|
||||
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.domain.Persistable;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -11,26 +16,31 @@ import java.time.LocalDateTime;
|
||||
* @author 张翔
|
||||
* @date 2026-03-13
|
||||
*/
|
||||
public abstract class BaseEntity {
|
||||
public abstract class BaseEntity implements Persistable<Long> {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@CreatedBy
|
||||
@Column("create_by")
|
||||
private String createBy;
|
||||
|
||||
@LastModifiedBy
|
||||
@Column("update_by")
|
||||
private String updateBy;
|
||||
|
||||
@CreatedDate
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column("deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -78,4 +88,13 @@ public abstract class BaseEntity {
|
||||
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断实体是否为新的
|
||||
* 如果createdAt为null,则认为是新实体
|
||||
*/
|
||||
@Override
|
||||
public boolean isNew() {
|
||||
return createdAt == null;
|
||||
}
|
||||
}
|
||||
|
||||
+134
@@ -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);
|
||||
}
|
||||
}
|
||||
+51
@@ -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));
|
||||
+46
@@ -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;
|
||||
+14
-14
@@ -3,7 +3,7 @@
|
||||
-- 描述: 创建所有核心表结构
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS sys_user (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100),
|
||||
@@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS sys_user (
|
||||
);
|
||||
-- 角色表
|
||||
CREATE TABLE IF NOT EXISTS sys_role (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
role_name VARCHAR(100) NOT NULL,
|
||||
role_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
role_sort INTEGER DEFAULT 0,
|
||||
@@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS sys_role (
|
||||
);
|
||||
-- 菜单表(统一使用sys_menu表名)
|
||||
CREATE TABLE IF NOT EXISTS sys_menu (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
menu_name VARCHAR(50) NOT NULL,
|
||||
parent_id BIGINT DEFAULT 0,
|
||||
order_num INTEGER DEFAULT 0,
|
||||
@@ -48,7 +48,7 @@ CREATE TABLE IF NOT EXISTS sys_menu (
|
||||
);
|
||||
-- 字典类型表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_type (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
dict_name VARCHAR(100) NOT NULL,
|
||||
dict_type VARCHAR(100) NOT NULL UNIQUE,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
@@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS sys_dict_type (
|
||||
);
|
||||
-- 字典数据表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_data (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
dict_sort INTEGER DEFAULT 0,
|
||||
dict_label VARCHAR(100) NOT NULL,
|
||||
dict_value VARCHAR(100) NOT NULL,
|
||||
@@ -78,7 +78,7 @@ CREATE TABLE IF NOT EXISTS sys_dict_data (
|
||||
);
|
||||
-- 字典表(通用字典)
|
||||
CREATE TABLE IF NOT EXISTS sys_dictionary (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
type VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
@@ -92,7 +92,7 @@ CREATE TABLE IF NOT EXISTS sys_dictionary (
|
||||
);
|
||||
-- 系统配置表
|
||||
CREATE TABLE IF NOT EXISTS sys_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
config_name VARCHAR(100) NOT NULL,
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
config_value VARCHAR(500) NOT NULL,
|
||||
@@ -105,7 +105,7 @@ CREATE TABLE IF NOT EXISTS sys_config (
|
||||
);
|
||||
-- 登录日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_login_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
ip VARCHAR(50),
|
||||
location VARCHAR(255),
|
||||
@@ -117,7 +117,7 @@ CREATE TABLE IF NOT EXISTS sys_login_log (
|
||||
);
|
||||
-- 异常日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_exception_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
title VARCHAR(100),
|
||||
exception_name VARCHAR(100),
|
||||
@@ -130,7 +130,7 @@ CREATE TABLE IF NOT EXISTS sys_exception_log (
|
||||
);
|
||||
-- 操作日志表
|
||||
CREATE TABLE IF NOT EXISTS operation_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
operation VARCHAR(100),
|
||||
method VARCHAR(200),
|
||||
@@ -148,7 +148,7 @@ CREATE TABLE IF NOT EXISTS operation_log (
|
||||
);
|
||||
-- 系统公告表
|
||||
CREATE TABLE IF NOT EXISTS sys_notice (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
notice_title VARCHAR(50) NOT NULL,
|
||||
notice_type VARCHAR(1) NOT NULL,
|
||||
notice_content TEXT,
|
||||
@@ -161,7 +161,7 @@ CREATE TABLE IF NOT EXISTS sys_notice (
|
||||
);
|
||||
-- 用户消息表
|
||||
CREATE TABLE IF NOT EXISTS sys_user_message (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
notice_id BIGINT,
|
||||
message_title VARCHAR(255),
|
||||
@@ -176,7 +176,7 @@ CREATE TABLE IF NOT EXISTS sys_user_message (
|
||||
);
|
||||
-- 文件管理表
|
||||
CREATE TABLE IF NOT EXISTS sys_file (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
file_size BIGINT,
|
||||
@@ -191,7 +191,7 @@ CREATE TABLE IF NOT EXISTS sys_file (
|
||||
);
|
||||
-- OAuth2客户端表
|
||||
CREATE TABLE IF NOT EXISTS oauth2_client (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id BIGINT PRIMARY KEY,
|
||||
client_id VARCHAR(100) NOT NULL UNIQUE,
|
||||
client_secret VARCHAR(255) NOT NULL,
|
||||
client_name VARCHAR(100),
|
||||
|
||||
+12
-4
@@ -3,18 +3,26 @@
|
||||
-- 描述: 插入必要的初始数据
|
||||
|
||||
-- 插入初始角色
|
||||
INSERT INTO roles (role_name, role_key, role_sort, status, create_by, update_by)
|
||||
INSERT INTO sys_role (role_name, role_key, role_sort, status, create_by, update_by)
|
||||
VALUES ('超级管理员', 'admin', 1, 1, 'system', 'system')
|
||||
ON CONFLICT (role_key) DO NOTHING;
|
||||
|
||||
-- 插入初始管理员用户
|
||||
-- BCrypt哈希值对应明文密码: admin123
|
||||
INSERT INTO users (id, username, password, email, phone, status, create_by, update_by)
|
||||
INSERT INTO sys_user (id, username, password, email, phone, status, create_by, update_by)
|
||||
VALUES (1, 'admin', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'admin@novalon.com', '13800138000', 1, 'system', 'system')
|
||||
ON CONFLICT (username) DO UPDATE SET
|
||||
password = EXCLUDED.password,
|
||||
status = EXCLUDED.status;
|
||||
|
||||
-- 插入测试用户(用于E2E测试)
|
||||
-- BCrypt哈希值对应明文密码: admin123
|
||||
INSERT INTO sys_user (id, username, password, email, phone, status, create_by, update_by)
|
||||
VALUES (2, 'user', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'user@novalon.com', '13800138001', 1, 'system', 'system')
|
||||
ON CONFLICT (username) DO UPDATE SET
|
||||
password = EXCLUDED.password,
|
||||
status = EXCLUDED.status;
|
||||
|
||||
-- 插入初始字典类型
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by)
|
||||
VALUES
|
||||
@@ -52,8 +60,8 @@ VALUES
|
||||
ON CONFLICT (config_key) DO NOTHING;
|
||||
|
||||
-- 重置序列值
|
||||
SELECT setval('users_id_seq', (SELECT COALESCE(MAX(id), 1) FROM users));
|
||||
SELECT setval('roles_id_seq', (SELECT COALESCE(MAX(id), 1) FROM roles));
|
||||
SELECT setval('sys_user_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_user));
|
||||
SELECT setval('sys_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role));
|
||||
SELECT setval('sys_dict_type_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_type));
|
||||
SELECT setval('sys_dict_data_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_data));
|
||||
SELECT setval('sys_config_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_config));
|
||||
+1
-1
@@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS sys_role_permission (
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
|
||||
UNIQUE (role_id, permission_id)
|
||||
);
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
-- 描述: 为表创建必要的索引以提升查询性能
|
||||
|
||||
-- 用户表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON sys_user(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON sys_user(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_status ON sys_user(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON sys_user(deleted_at);
|
||||
|
||||
-- 角色表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_role_key ON roles(role_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_status ON roles(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON roles(deleted_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_role_key ON sys_role(role_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_status ON sys_role(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON sys_role(deleted_at);
|
||||
|
||||
-- 菜单表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
|
||||
|
||||
+7
-8
@@ -1,7 +1,6 @@
|
||||
-- Novalon管理系统审计日志表
|
||||
-- 版本: V7
|
||||
-- 描述: 创建审计日志表,记录数据变更前后的完整对比
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
entity_type VARCHAR(100) NOT NULL,
|
||||
@@ -11,20 +10,22 @@ CREATE TABLE IF NOT EXISTS audit_log (
|
||||
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
before_data JSONB,
|
||||
after_data JSONB,
|
||||
changed_fields TEXT[],
|
||||
changed_fields TEXT [],
|
||||
ip_address VARCHAR(50),
|
||||
user_agent TEXT,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_log_entity_type ON audit_log(entity_type);
|
||||
CREATE INDEX idx_audit_log_entity_id ON audit_log(entity_id);
|
||||
CREATE INDEX idx_audit_log_operation_type ON audit_log(operation_type);
|
||||
CREATE INDEX idx_audit_log_operator ON audit_log(operator);
|
||||
CREATE INDEX idx_audit_log_operation_time ON audit_log(operation_time);
|
||||
CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id);
|
||||
|
||||
COMMENT ON TABLE audit_log IS '审计日志表';
|
||||
COMMENT ON COLUMN audit_log.id IS '主键ID';
|
||||
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)';
|
||||
@@ -35,7 +36,5 @@ COMMENT ON COLUMN audit_log.operation_time IS '操作时间';
|
||||
COMMENT ON COLUMN audit_log.before_data IS '变更前数据(JSON格式)';
|
||||
COMMENT ON COLUMN audit_log.after_data IS '变更后数据(JSON格式)';
|
||||
COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表';
|
||||
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';
|
||||
COMMENT ON COLUMN audit_log.user_agent IS '用户代理';
|
||||
COMMENT ON COLUMN audit_log.description IS '操作描述';
|
||||
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';COMMENT ON COLUMN audit_log.description IS '操作描述';
|
||||
COMMENT ON COLUMN audit_log.created_at IS '记录创建时间';
|
||||
|
||||
+13
@@ -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;
|
||||
+16
-6
@@ -1,7 +1,7 @@
|
||||
package cn.novalon.manage.sys.audit;
|
||||
|
||||
import cn.novalon.manage.sys.audit.domain.AuditLog;
|
||||
import cn.novalon.manage.sys.audit.repository.IAuditLogRepository;
|
||||
import cn.novalon.manage.sys.audit.service.IAuditLogService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
@@ -34,11 +34,11 @@ public class AuditLogAspect {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuditLogAspect.class);
|
||||
|
||||
private final IAuditLogRepository auditLogRepository;
|
||||
private final IAuditLogService auditLogService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public AuditLogAspect(IAuditLogRepository auditLogRepository, ObjectMapper objectMapper) {
|
||||
this.auditLogRepository = auditLogRepository;
|
||||
public AuditLogAspect(IAuditLogService auditLogService, ObjectMapper objectMapper) {
|
||||
this.auditLogService = auditLogService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@@ -99,6 +99,9 @@ public class AuditLogAspect {
|
||||
String finalOperationType = operationTypeHolder[0];
|
||||
String finalBeforeData = beforeDataHolder[0];
|
||||
|
||||
logger.debug("保存操作审计日志: entityType={}, entityIdHolder={}, extractedEntityId={}, finalEntityId={}",
|
||||
entityType, entityIdHolder[0], extractEntityId(savedEntity), finalEntityId);
|
||||
|
||||
return createAndSaveAuditLog(
|
||||
entityType, finalEntityId, finalOperationType,
|
||||
finalBeforeData, afterData, savedEntity
|
||||
@@ -163,18 +166,22 @@ public class AuditLogAspect {
|
||||
private Mono<Void> createAndSaveAuditLog(String entityType, Long entityId,
|
||||
String operationType, String beforeData,
|
||||
String afterData, Object entity) {
|
||||
logger.debug("创建审计日志: entityType={}, entityId={}, operationType={}", entityType, entityId, operationType);
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(ctx -> ctx.getAuthentication().getPrincipal())
|
||||
.defaultIfEmpty("system")
|
||||
.flatMap(principal -> {
|
||||
AuditLog auditLog = new AuditLog();
|
||||
auditLog.setEntityType(entityType);
|
||||
auditLog.setEntityId(entityId);
|
||||
auditLog.setEntityId(entityId != null ? entityId : 0L);
|
||||
auditLog.setOperationType(operationType);
|
||||
auditLog.setOperator(principal instanceof String ? (String) principal : "system");
|
||||
auditLog.setBeforeData(beforeData);
|
||||
auditLog.setAfterData(afterData);
|
||||
|
||||
logger.debug("审计日志对象: entityId={}, entityType={}, operationType={}",
|
||||
auditLog.getEntityId(), auditLog.getEntityType(), auditLog.getOperationType());
|
||||
|
||||
if (beforeData != null && afterData != null) {
|
||||
String[] changedFields = extractChangedFields(beforeData, afterData);
|
||||
auditLog.setChangedFields(changedFields);
|
||||
@@ -182,7 +189,7 @@ public class AuditLogAspect {
|
||||
|
||||
auditLog.setDescription(generateDescription(entityType, operationType, entityId));
|
||||
|
||||
return auditLogRepository.save(auditLog)
|
||||
return auditLogService.save(auditLog)
|
||||
.doOnSuccess(saved -> logger.debug("审计日志保存成功: {} - {}",
|
||||
entityType, operationType))
|
||||
.doOnError(error -> logger.error("审计日志保存失败: {}",
|
||||
@@ -231,11 +238,14 @@ public class AuditLogAspect {
|
||||
}
|
||||
|
||||
private Long extractEntityId(Object entity) {
|
||||
logger.debug("提取实体ID: entity class={}", entity.getClass().getName());
|
||||
if (entity instanceof Persistable) {
|
||||
Persistable<?> persistable = (Persistable<?>) entity;
|
||||
Object id = persistable.getId();
|
||||
logger.debug("Persistable实体ID: id={}, isNew={}", id, persistable.isNew());
|
||||
return id != null ? ((Number) id).longValue() : null;
|
||||
}
|
||||
logger.debug("实体不是Persistable类型");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+2
-41
@@ -1,9 +1,7 @@
|
||||
package cn.novalon.manage.sys.audit.domain;
|
||||
|
||||
import cn.novalon.manage.sys.core.domain.BaseDomain;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -13,73 +11,44 @@ import java.time.LocalDateTime;
|
||||
* @author 张翔
|
||||
* @date 2026-04-01
|
||||
*/
|
||||
@Table("audit_log")
|
||||
@Schema(description = "审计日志实体")
|
||||
public class AuditLog {
|
||||
public class AuditLog extends BaseDomain {
|
||||
|
||||
@Id
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@Column("entity_type")
|
||||
@Schema(description = "实体类型(如User, Role等)", example = "User")
|
||||
private String entityType;
|
||||
|
||||
@Column("entity_id")
|
||||
@Schema(description = "实体ID", example = "1")
|
||||
private Long entityId;
|
||||
|
||||
@Column("operation_type")
|
||||
@Schema(description = "操作类型(CREATE, UPDATE, DELETE)", example = "UPDATE")
|
||||
private String operationType;
|
||||
|
||||
@Column("operator")
|
||||
@Schema(description = "操作人", example = "admin")
|
||||
private String operator;
|
||||
|
||||
@Column("operation_time")
|
||||
@Schema(description = "操作时间")
|
||||
private LocalDateTime operationTime;
|
||||
|
||||
@Column("before_data")
|
||||
@Schema(description = "变更前数据(JSON格式)")
|
||||
private String beforeData;
|
||||
|
||||
@Column("after_data")
|
||||
@Schema(description = "变更后数据(JSON格式)")
|
||||
private String afterData;
|
||||
|
||||
@Column("changed_fields")
|
||||
@Schema(description = "变更字段列表")
|
||||
private String[] changedFields;
|
||||
|
||||
@Column("ip_address")
|
||||
@Schema(description = "IP地址", example = "192.168.1.100")
|
||||
private String ipAddress;
|
||||
|
||||
@Column("user_agent")
|
||||
@Schema(description = "用户代理")
|
||||
private String userAgent;
|
||||
|
||||
@Column("description")
|
||||
@Schema(description = "操作描述", example = "更新用户信息")
|
||||
private String description;
|
||||
|
||||
@Column("created_at")
|
||||
@Schema(description = "记录创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public AuditLog() {
|
||||
this.operationTime = LocalDateTime.now();
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getEntityType() {
|
||||
@@ -169,12 +138,4 @@ public class AuditLog {
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -1,8 +1,6 @@
|
||||
package cn.novalon.manage.sys.audit.repository;
|
||||
|
||||
import cn.novalon.manage.sys.audit.domain.AuditLog;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -14,8 +12,15 @@ import java.time.LocalDateTime;
|
||||
* @author 张翔
|
||||
* @date 2026-04-01
|
||||
*/
|
||||
@Repository
|
||||
public interface IAuditLogRepository extends R2dbcRepository<AuditLog, Long> {
|
||||
public interface IAuditLogRepository {
|
||||
|
||||
Mono<AuditLog> findById(Long id);
|
||||
|
||||
Mono<AuditLog> save(AuditLog auditLog);
|
||||
|
||||
Mono<Void> deleteById(Long id);
|
||||
|
||||
Flux<AuditLog> findAll();
|
||||
|
||||
Flux<AuditLog> findByEntityType(String entityType);
|
||||
|
||||
|
||||
+30
@@ -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);
|
||||
}
|
||||
+68
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -1,5 +1,6 @@
|
||||
package cn.novalon.manage.sys.core.domain;
|
||||
|
||||
import cn.novalon.manage.common.util.SnowflakeId;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
@@ -64,4 +65,14 @@ public abstract class BaseDomain {
|
||||
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成主键ID
|
||||
*
|
||||
* @return 主键ID
|
||||
*/
|
||||
public Long generateId() {
|
||||
this.id = SnowflakeId.nextId();
|
||||
return this.id;
|
||||
}
|
||||
}
|
||||
|
||||
-10
@@ -78,16 +78,6 @@ public class SysPermission extends BaseDomain {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成主键ID
|
||||
*
|
||||
* @return 主键ID
|
||||
*/
|
||||
public Long generateId() {
|
||||
this.id = SnowflakeId.nextId();
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除权限
|
||||
*/
|
||||
|
||||
-10
@@ -58,16 +58,6 @@ public class SysRole extends BaseDomain {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成主键ID
|
||||
*
|
||||
* @return 主键ID
|
||||
*/
|
||||
public Long generateId() {
|
||||
this.id = SnowflakeId.nextId();
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
|
||||
-10
@@ -33,14 +33,4 @@ public class SysRolePermission extends BaseDomain {
|
||||
public void setPermissionId(Long permissionId) {
|
||||
this.permissionId = permissionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成主键ID
|
||||
*
|
||||
* @return 主键ID
|
||||
*/
|
||||
public Long generateId() {
|
||||
this.id = SnowflakeId.nextId();
|
||||
return this.id;
|
||||
}
|
||||
}
|
||||
-10
@@ -101,16 +101,6 @@ public class SysUser extends BaseDomain {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成主键ID
|
||||
*
|
||||
* @return 主键ID
|
||||
*/
|
||||
public Long generateId() {
|
||||
this.id = SnowflakeId.nextId();
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
|
||||
+1
@@ -82,6 +82,7 @@ public class SysRoleService implements ISysRoleService {
|
||||
@Override
|
||||
public Mono<SysRole> createRole(CreateRoleCommand command) {
|
||||
SysRole role = new SysRole();
|
||||
role.generateId();
|
||||
role.setRoleName(command.roleName());
|
||||
role.setRoleKey(command.roleKey());
|
||||
role.setRoleSort(command.roleSort());
|
||||
|
||||
+22
-23
@@ -44,15 +44,15 @@ public class SysUserService implements ISysUserService {
|
||||
private final IUserRoleRepository userRoleRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public SysUserService(ISysUserRepository userRepository,
|
||||
ISysRoleRepository roleRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder) {
|
||||
public SysUserService(ISysUserRepository userRepository,
|
||||
ISysRoleRepository roleRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.roleRepository = roleRepository;
|
||||
this.userRoleRepository = userRoleRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
|
||||
|
||||
logger.info("使用的密码编码器类型: {}", passwordEncoder.getClass().getName());
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ public class SysUserService implements ISysUserService {
|
||||
logger.info("SysUserService.createUser - 用户名: {}, 密码前缀: {}",
|
||||
user.getUsername(),
|
||||
user.getPassword() != null ? user.getPassword().substring(0, 7) : "null");
|
||||
user.generateId();
|
||||
if (user.getPassword() != null && !user.getPassword().startsWith("$2a$")
|
||||
&& !user.getPassword().startsWith("$2b$")) {
|
||||
logger.info("密码不以$2a$或$2b$开头,重新编码");
|
||||
@@ -106,7 +107,6 @@ public class SysUserService implements ISysUserService {
|
||||
} else {
|
||||
logger.info("密码已编码,跳过重新编码");
|
||||
}
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
if (user.getStatus() == null) {
|
||||
user.setStatus(StatusConstants.ENABLED);
|
||||
}
|
||||
@@ -116,6 +116,7 @@ public class SysUserService implements ISysUserService {
|
||||
@Override
|
||||
public Mono<SysUser> createUser(CreateUserCommand command) {
|
||||
SysUser user = new SysUser();
|
||||
user.generateId();
|
||||
user.setUsername(command.username().getValue());
|
||||
user.setPassword(passwordEncoder.encode(command.password().getValue()));
|
||||
user.setEmail(command.email().getValue());
|
||||
@@ -123,7 +124,6 @@ public class SysUserService implements ISysUserService {
|
||||
user.setPhone(command.phone());
|
||||
user.setRoleId(command.roleId());
|
||||
user.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ public class SysUserService implements ISysUserService {
|
||||
@Transactional
|
||||
public Mono<Void> deleteUser(Long id) {
|
||||
logger.debug("开始删除用户,ID: {}", id);
|
||||
|
||||
|
||||
return userRepository.findById(id)
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("User not found")))
|
||||
.flatMap(user -> {
|
||||
@@ -244,31 +244,30 @@ public class SysUserService implements ISysUserService {
|
||||
@Transactional
|
||||
public Mono<Void> assignRolesToUser(Long userId, List<Long> roleIds) {
|
||||
logger.debug("开始为用户分配角色,用户ID: {}, 角色IDs: {}", userId, roleIds);
|
||||
|
||||
|
||||
if (roleIds == null || roleIds.isEmpty()) {
|
||||
logger.debug("角色列表为空,删除用户的所有角色关联");
|
||||
return userRoleRepository.deleteByUserId(userId)
|
||||
.doOnSuccess(v -> logger.debug("成功删除用户的所有角色关联"))
|
||||
.doOnError(e -> logger.error("删除用户角色关联失败", e));
|
||||
}
|
||||
|
||||
|
||||
return userRoleRepository.deleteByUserId(userId)
|
||||
.doOnSuccess(v -> logger.debug("成功删除用户的旧角色关联"))
|
||||
.doOnError(e -> logger.error("删除用户旧角色关联失败", e))
|
||||
.then(
|
||||
Flux.fromIterable(roleIds)
|
||||
.concatMap(roleId -> {
|
||||
logger.debug("为用户分配角色ID: {}", roleId);
|
||||
UserRole userRole = new UserRole();
|
||||
userRole.setUserId(userId);
|
||||
userRole.setRoleId(roleId);
|
||||
userRole.setCreatedAt(LocalDateTime.now());
|
||||
return userRoleRepository.save(userRole)
|
||||
.doOnSuccess(v -> logger.debug("成功保存用户角色关联"))
|
||||
.doOnError(e -> logger.error("保存用户角色关联失败", e));
|
||||
})
|
||||
.then()
|
||||
);
|
||||
Flux.fromIterable(roleIds)
|
||||
.concatMap(roleId -> {
|
||||
logger.debug("为用户分配角色ID: {}", roleId);
|
||||
UserRole userRole = new UserRole();
|
||||
userRole.setUserId(userId);
|
||||
userRole.setRoleId(roleId);
|
||||
userRole.setCreatedAt(LocalDateTime.now());
|
||||
return userRoleRepository.save(userRole)
|
||||
.doOnSuccess(v -> logger.debug("成功保存用户角色关联"))
|
||||
.doOnError(e -> logger.error("保存用户角色关联失败", e));
|
||||
})
|
||||
.then());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+15
@@ -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;
|
||||
}
|
||||
}
|
||||
+13
@@ -6,6 +6,8 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户注册请求DTO
|
||||
*
|
||||
@@ -42,6 +44,9 @@ public class UserRegisterRequest {
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "角色ID列表", example = "[1, 2]")
|
||||
private List<Long> roles;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
@@ -81,4 +86,12 @@ public class UserRegisterRequest {
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public List<Long> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(List<Long> roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -3,6 +3,7 @@ package cn.novalon.manage.sys.handler.user;
|
||||
import cn.novalon.manage.sys.core.domain.SysUser;
|
||||
import cn.novalon.manage.sys.core.service.ISysUserService;
|
||||
import cn.novalon.manage.common.dto.PageRequest;
|
||||
import cn.novalon.manage.sys.dto.request.AssignRolesRequest;
|
||||
import cn.novalon.manage.sys.dto.request.PasswordChangeRequest;
|
||||
import cn.novalon.manage.sys.dto.request.UserRegisterRequest;
|
||||
import cn.novalon.manage.sys.dto.request.UserUpdateRequest;
|
||||
@@ -135,6 +136,14 @@ public class SysUserHandler {
|
||||
null
|
||||
))
|
||||
.flatMap(userService::createUser)
|
||||
.flatMap(user -> {
|
||||
if (req.getRoles() != null && !req.getRoles().isEmpty()) {
|
||||
logger.info("为用户 {} 分配角色: {}", user.getUsername(), req.getRoles());
|
||||
return userService.assignRolesToUser(user.getId(), req.getRoles())
|
||||
.then(Mono.just(user));
|
||||
}
|
||||
return Mono.just(user);
|
||||
})
|
||||
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
|
||||
});
|
||||
}
|
||||
@@ -249,9 +258,8 @@ public class SysUserHandler {
|
||||
@OperationLog(operation = "分配角色", module = "用户管理")
|
||||
public Mono<ServerResponse> assignRoles(ServerRequest request) {
|
||||
Long id = Long.valueOf(request.pathVariable("id"));
|
||||
return request.bodyToMono(new org.springframework.core.ParameterizedTypeReference<List<Long>>() {
|
||||
})
|
||||
.flatMap(roleIds -> userService.assignRolesToUser(id, roleIds))
|
||||
return request.bodyToMono(AssignRolesRequest.class)
|
||||
.flatMap(req -> userService.assignRolesToUser(id, req.getRoleIds()))
|
||||
.then(ServerResponse.ok().build())
|
||||
.onErrorResume(error -> {
|
||||
logger.error("分配角色失败", error);
|
||||
|
||||
@@ -34,3 +34,6 @@ TEST_WORKERS=4
|
||||
# 测试报告配置(可选)
|
||||
TEST_REPORT_FOLDER=playwright-report
|
||||
TEST_RESULTS_FOLDER=test-results
|
||||
|
||||
# API签名密钥配置
|
||||
VITE_SIGNATURE_SECRET=your-secret-key-here
|
||||
|
||||
@@ -2,7 +2,10 @@ node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
coverage
|
||||
.nyc_output
|
||||
debug-*.png
|
||||
e2e/debug/
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
FROM node:18-alpine AS builder
|
||||
# 构建阶段
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
# 安装 pnpm
|
||||
RUN npm install -g pnpm@8.15.0
|
||||
|
||||
# 复制 package.json 和 lock 文件
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# 安装依赖
|
||||
RUN pnpm install
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# 构建生产版本
|
||||
RUN pnpm run build:prod
|
||||
|
||||
# 生产阶段
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
# 复制自定义 nginx 配置
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
# 启动 nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@@ -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"]
|
||||
@@ -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. 优先使用单元测试覆盖功能细节
|
||||
@@ -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('返回结果');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -36,17 +36,32 @@ class CustomReporter implements Reporter {
|
||||
}
|
||||
|
||||
private calculateStats(result: FullResult): TestStats {
|
||||
const suites = result.suites || [];
|
||||
const allTests = suites.flatMap(suite =>
|
||||
suite.specs.flatMap(spec => spec.tests)
|
||||
);
|
||||
const allTests = this.testResults;
|
||||
|
||||
if (allTests.length === 0) {
|
||||
return {
|
||||
total: 0,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
flaky: 0,
|
||||
passRate: 0,
|
||||
failRate: 0,
|
||||
skipRate: 0,
|
||||
flakyRate: 0,
|
||||
totalDuration: 0,
|
||||
avgDuration: 0,
|
||||
slowestTests: [],
|
||||
failedTests: [],
|
||||
};
|
||||
}
|
||||
|
||||
const passed = allTests.filter(t => t.status === 'passed');
|
||||
const failed = allTests.filter(t => t.status === 'failed');
|
||||
const skipped = allTests.filter(t => t.status === 'skipped');
|
||||
const flaky = allTests.filter(t => t.status === 'passed' && t.retry >= 1);
|
||||
|
||||
const totalDuration = allTests.reduce((sum, t) => sum + (t.duration || 0), 0);
|
||||
const totalDuration = allTests.reduce((sum, t) => sum + t.duration, 0);
|
||||
const avgDuration = totalDuration / allTests.length;
|
||||
|
||||
const passRate = (passed.length / allTests.length) * 100;
|
||||
@@ -67,8 +82,8 @@ class CustomReporter implements Reporter {
|
||||
totalDuration,
|
||||
avgDuration,
|
||||
slowestTests: allTests
|
||||
.filter(t => t.duration)
|
||||
.sort((a, b) => (b.duration || 0) - (a.duration || 0))
|
||||
.filter(t => t.duration > 0)
|
||||
.sort((a, b) => b.duration - a.duration)
|
||||
.slice(0, 10),
|
||||
failedTests: failed,
|
||||
};
|
||||
@@ -98,9 +113,13 @@ class CustomReporter implements Reporter {
|
||||
if (stats.failedTests.length > 0) {
|
||||
console.log('❌ 失败的测试:');
|
||||
stats.failedTests.forEach((test, index) => {
|
||||
console.log(` ${index + 1}. ${test.title}`);
|
||||
console.log(` 位置: ${test.location.file}:${test.location.line}`);
|
||||
console.log(` 错误: ${test.error?.message}`);
|
||||
console.log(` ${index + 1}. ${test.title || '未命名测试'}`);
|
||||
if (test.location?.file) {
|
||||
console.log(` 位置: ${test.location.file}:${test.location.line || 0}`);
|
||||
}
|
||||
if (test.error?.message) {
|
||||
console.log(` 错误: ${test.error.message}`);
|
||||
}
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
|
||||
test.describe('Dashboard操作日志显示验证', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await loginPage.logout();
|
||||
});
|
||||
|
||||
test('Dashboard应显示操作日志统计卡片', async ({ page }) => {
|
||||
await test.step('验证操作日志统计卡片存在', async () => {
|
||||
const operationLogCard = page.locator('.stat-card.log-card');
|
||||
await expect(operationLogCard).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证操作日志统计标题', async () => {
|
||||
const title = page.locator('.stat-card.log-card .el-statistic__head');
|
||||
await expect(title).toContainText('操作日志');
|
||||
});
|
||||
|
||||
await test.step('验证操作日志统计数值', async () => {
|
||||
const value = page.locator('.stat-card.log-card .el-statistic__number');
|
||||
await expect(value).toBeVisible();
|
||||
const countText = await value.textContent();
|
||||
expect(countText).not.toBeNull();
|
||||
const count = parseInt(countText!);
|
||||
expect(count).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('Dashboard应显示其他统计卡片', async ({ page }) => {
|
||||
await test.step('验证用户总数卡片', async () => {
|
||||
const userCard = page.locator('.stat-card.user-card');
|
||||
await expect(userCard).toBeVisible();
|
||||
const title = userCard.locator('.el-statistic__head');
|
||||
await expect(title).toContainText('用户总数');
|
||||
});
|
||||
|
||||
await test.step('验证角色总数卡片', async () => {
|
||||
const roleCard = page.locator('.stat-card.role-card');
|
||||
await expect(roleCard).toBeVisible();
|
||||
const title = roleCard.locator('.el-statistic__head');
|
||||
await expect(title).toContainText('角色总数');
|
||||
});
|
||||
|
||||
await test.step('验证今日登录卡片', async () => {
|
||||
const loginCard = page.locator('.stat-card.login-card');
|
||||
await expect(loginCard).toBeVisible();
|
||||
const title = loginCard.locator('.el-statistic__head');
|
||||
await expect(title).toContainText('今日登录');
|
||||
});
|
||||
});
|
||||
|
||||
test('Dashboard统计卡片应显示图标', async ({ page }) => {
|
||||
await test.step('验证操作日志图标', async () => {
|
||||
const icon = page.locator('.stat-card.log-card .stat-icon');
|
||||
await expect(icon).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证用户图标', async () => {
|
||||
const icon = page.locator('.stat-card.user-card .stat-icon');
|
||||
await expect(icon).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证角色图标', async () => {
|
||||
const icon = page.locator('.stat-card.role-card .stat-icon');
|
||||
await expect(icon).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证登录图标', async () => {
|
||||
const icon = page.locator('.stat-card.login-card .stat-icon');
|
||||
await expect(icon).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('Dashboard统计卡片应有悬停效果', async ({ page }) => {
|
||||
await test.step('验证操作日志卡片悬停效果', async () => {
|
||||
const card = page.locator('.stat-card.log-card');
|
||||
await card.hover();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(card).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('Dashboard应显示最近登录记录', async ({ page }) => {
|
||||
await test.step('验证最近登录卡片存在', async () => {
|
||||
const recentLoginCard = page.locator('.recent-login-card');
|
||||
await expect(recentLoginCard).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证最近登录标题', async () => {
|
||||
const title = page.locator('.recent-login-card .card-title');
|
||||
await expect(title).toContainText('最近登录');
|
||||
});
|
||||
});
|
||||
|
||||
test('Dashboard应显示系统信息', async ({ page }) => {
|
||||
await test.step('验证系统信息卡片存在', async () => {
|
||||
const systemInfoCard = page.locator('.system-info-card');
|
||||
await expect(systemInfoCard).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证系统信息标题', async () => {
|
||||
const title = page.locator('.system-info-card .card-title');
|
||||
await expect(title).toContainText('系统信息');
|
||||
});
|
||||
|
||||
await test.step('验证系统版本显示', async () => {
|
||||
const versionItem = page.locator('.system-info-card').getByText('系统版本');
|
||||
await expect(versionItem).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证Java版本显示', async () => {
|
||||
const javaItem = page.locator('.system-info-card').getByText('Java版本');
|
||||
await expect(javaItem).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证前端框架显示', async () => {
|
||||
const frontendItem = page.locator('.system-info-card').getByText('前端框架');
|
||||
await expect(frontendItem).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证数据库显示', async () => {
|
||||
const dbItem = page.locator('.system-info-card').getByText('数据库');
|
||||
await expect(dbItem).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('Dashboard操作日志统计应正确反映实际数据', async ({ page }) => {
|
||||
await test.step('获取Dashboard显示的操作日志数量', async () => {
|
||||
const value = page.locator('.stat-card.log-card .el-statistic__number');
|
||||
await expect(value).toBeVisible();
|
||||
const countText = await value.textContent();
|
||||
expect(countText).not.toBeNull();
|
||||
const dashboardCount = parseInt(countText!);
|
||||
expect(dashboardCount).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('Dashboard页面加载性能', async ({ page }) => {
|
||||
await test.step('验证页面加载时间', async () => {
|
||||
const startTime = Date.now();
|
||||
await dashboardPage.goto();
|
||||
const loadTime = Date.now() - startTime;
|
||||
expect(loadTime).toBeLessThan(10000);
|
||||
});
|
||||
|
||||
await test.step('验证统计卡片加载', async () => {
|
||||
const cards = page.locator('.stat-card');
|
||||
await expect(cards.first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('Dashboard响应式布局验证', async ({ page }) => {
|
||||
await test.step('验证桌面端布局', async () => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
const cards = page.locator('.stat-card');
|
||||
expect(await cards.count()).toBe(4);
|
||||
});
|
||||
|
||||
await test.step('验证平板端布局', async () => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
const cards = page.locator('.stat-card');
|
||||
expect(await cards.count()).toBe(4);
|
||||
});
|
||||
|
||||
await test.step('验证移动端布局', async () => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
const cards = page.locator('.stat-card');
|
||||
expect(await cards.count()).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
|
||||
test.describe('调试测试 - 网络请求监控', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
let userManagementPage: UserManagementPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
userManagementPage = new UserManagementPage(page);
|
||||
|
||||
// 监控所有网络请求
|
||||
page.on('request', request => {
|
||||
console.log(`>> REQUEST: ${request.method()} ${request.url()}`);
|
||||
if (request.method() === 'POST' || request.method() === 'PUT') {
|
||||
console.log(` POST DATA: ${request.postData()}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
console.log(`<< RESPONSE: ${response.status()} ${response.url()}`);
|
||||
if (response.status() >= 400) {
|
||||
console.log(` ❌ ERROR RESPONSE: ${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 清理localStorage
|
||||
await page.goto('/');
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
|
||||
// 重新登录
|
||||
await loginPage.goto();
|
||||
await loginPage.login('e2e_test_user', 'admin123');
|
||||
});
|
||||
|
||||
test('创建用户 - 带网络监控', async ({ page }) => {
|
||||
console.log('\n========== 开始创建用户测试 ==========\n');
|
||||
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
console.log('✅ 导航到用户管理页面');
|
||||
|
||||
await userManagementPage.clickCreateUser();
|
||||
console.log('✅ 点击创建用户按钮');
|
||||
|
||||
const timestamp = Date.now();
|
||||
const userData = {
|
||||
username: `testuser_${timestamp}`,
|
||||
nickname: `测试用户${timestamp}`,
|
||||
email: `test_${timestamp}@example.com`,
|
||||
phone: '13800138000',
|
||||
password: 'Test123!@#',
|
||||
confirmPassword: 'Test123!@#',
|
||||
};
|
||||
|
||||
console.log(`📝 填写用户数据: ${JSON.stringify(userData)}`);
|
||||
await userManagementPage.fillUserForm(userData);
|
||||
console.log('✅ 填写用户表单完成');
|
||||
|
||||
console.log('📤 准备提交表单...');
|
||||
await userManagementPage.submitForm();
|
||||
console.log('✅ 表单已提交');
|
||||
|
||||
// 等待一段时间,观察网络请求
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
console.log('\n========== 测试结束 ==========\n');
|
||||
});
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
|
||||
test.describe('登录诊断测试', () => {
|
||||
test('诊断登录问题', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
|
||||
console.log('=== 开始诊断登录问题 ===');
|
||||
|
||||
await loginPage.goto();
|
||||
console.log('1. 登录页面加载成功');
|
||||
|
||||
await page.screenshot({ path: 'test-results/diagnostic/01-login-page.png', fullPage: true });
|
||||
console.log('2. 截图已保存: 01-login-page.png');
|
||||
|
||||
const usernameVisible = await loginPage.usernameInput.isVisible();
|
||||
const passwordVisible = await loginPage.passwordInput.isVisible();
|
||||
const loginButtonVisible = await loginPage.loginButton.isVisible();
|
||||
|
||||
console.log('3. 页面元素检查:');
|
||||
console.log(` - 用户名输入框: ${usernameVisible ? '可见' : '不可见'}`);
|
||||
console.log(` - 密码输入框: ${passwordVisible ? '可见' : '不可见'}`);
|
||||
console.log(` - 登录按钮: ${loginButtonVisible ? '可见' : '不可见'}`);
|
||||
|
||||
await loginPage.usernameInput.fill('admin');
|
||||
await loginPage.passwordInput.fill('admin123');
|
||||
console.log('4. 已填写用户名和密码');
|
||||
|
||||
await page.screenshot({ path: 'test-results/diagnostic/02-filled-form.png', fullPage: true });
|
||||
console.log('5. 截图已保存: 02-filled-form.png');
|
||||
|
||||
const responsePromise = page.waitForResponse(response =>
|
||||
response.url().includes('/api/auth/login') && response.request().method() === 'POST'
|
||||
);
|
||||
|
||||
await loginPage.loginButton.click();
|
||||
console.log('6. 已点击登录按钮');
|
||||
|
||||
try {
|
||||
const response = await responsePromise;
|
||||
console.log('7. 收到API响应:');
|
||||
console.log(` - 状态码: ${response.status()}`);
|
||||
console.log(` - URL: ${response.url()}`);
|
||||
|
||||
const responseBody = await response.text();
|
||||
console.log(` - 响应体: ${responseBody.substring(0, 500)}`);
|
||||
} catch (error) {
|
||||
console.log('7. 未收到API响应或超时:', error);
|
||||
}
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const currentUrl = page.url();
|
||||
console.log(`8. 当前URL: ${currentUrl}`);
|
||||
|
||||
await page.screenshot({ path: 'test-results/diagnostic/03-after-login.png', fullPage: true });
|
||||
console.log('9. 截图已保存: 03-after-login.png');
|
||||
|
||||
const errorMessage = await loginPage.getErrorMessage();
|
||||
if (errorMessage) {
|
||||
console.log(`10. 错误消息: ${errorMessage}`);
|
||||
} else {
|
||||
console.log('10. 没有错误消息');
|
||||
}
|
||||
|
||||
const pageContent = await page.content();
|
||||
console.log('11. 页面内容长度:', pageContent.length);
|
||||
|
||||
if (currentUrl.includes('dashboard')) {
|
||||
console.log('✅ 登录成功!已跳转到仪表板');
|
||||
} else if (currentUrl.includes('login')) {
|
||||
console.log('❌ 登录失败!仍在登录页面');
|
||||
} else {
|
||||
console.log(`⚠️ 意外的URL: ${currentUrl}`);
|
||||
}
|
||||
|
||||
console.log('=== 诊断完成 ===');
|
||||
});
|
||||
});
|
||||
@@ -1,481 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DictionaryManagementPage } from './pages/DictionaryManagementPage';
|
||||
|
||||
test.describe('字典管理 E2E 测试', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dictManagementPage: DictionaryManagementPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dictManagementPage = new DictionaryManagementPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
});
|
||||
|
||||
test('DICT-001: 访问字典管理页面', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
await expect(page).toHaveURL(/.*dict/);
|
||||
});
|
||||
|
||||
await test.step('验证页面元素可见', async () => {
|
||||
await expect(dictManagementPage.table).toBeVisible();
|
||||
await expect(dictManagementPage.createDictTypeButton).toBeVisible();
|
||||
await expect(dictManagementPage.searchInput).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-002: 创建字典类型', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('点击新增字典类型按钮', async () => {
|
||||
await dictManagementPage.clickCreateDictType();
|
||||
});
|
||||
|
||||
await test.step('填写字典类型信息', async () => {
|
||||
const timestamp = Date.now();
|
||||
const dictTypeData = {
|
||||
dictName: `测试字典类型_${timestamp}`,
|
||||
dictType: `test_dict_type_${timestamp}`,
|
||||
status: '1',
|
||||
remark: '这是一个测试字典类型'
|
||||
};
|
||||
await dictManagementPage.fillDictTypeForm(dictTypeData);
|
||||
});
|
||||
|
||||
await test.step('提交表单', async () => {
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
await test.step('验证字典类型创建成功', async () => {
|
||||
await dictManagementPage.reload();
|
||||
const dictTypeCount = await dictManagementPage.getDictTypeCount();
|
||||
expect(dictTypeCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-003: 编辑字典类型', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建测试字典类型', async () => {
|
||||
await dictManagementPage.clickCreateDictType();
|
||||
const timestamp = Date.now();
|
||||
const dictTypeData = {
|
||||
dictName: `待编辑字典_${timestamp}`,
|
||||
dictType: `edit_dict_${timestamp}`,
|
||||
status: '1'
|
||||
};
|
||||
await dictManagementPage.fillDictTypeForm(dictTypeData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
await test.step('编辑字典类型', async () => {
|
||||
const timestamp = Date.now();
|
||||
await dictManagementPage.editDictType(`待编辑字典_${timestamp}`);
|
||||
await page.waitForTimeout(500);
|
||||
const updateData = {
|
||||
dictName: `已编辑字典_${timestamp}`,
|
||||
remark: '这是更新后的备注'
|
||||
};
|
||||
await dictManagementPage.fillDictTypeForm(updateData);
|
||||
});
|
||||
|
||||
await test.step('提交修改', async () => {
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-004: 删除字典类型', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建测试字典类型', async () => {
|
||||
await dictManagementPage.clickCreateDictType();
|
||||
const timestamp = Date.now();
|
||||
const dictTypeData = {
|
||||
dictName: `待删除字典_${timestamp}`,
|
||||
dictType: `delete_dict_${timestamp}`,
|
||||
status: '1'
|
||||
};
|
||||
await dictManagementPage.fillDictTypeForm(dictTypeData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
await test.step('删除字典类型', async () => {
|
||||
const timestamp = Date.now();
|
||||
await dictManagementPage.deleteDictType(`待删除字典_${timestamp}`);
|
||||
await dictManagementPage.confirmDelete();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
await test.step('验证字典类型已删除', async () => {
|
||||
await dictManagementPage.reload();
|
||||
const timestamp = Date.now();
|
||||
const dictDeleted = await dictManagementPage.containsText(`待删除字典_${timestamp}`);
|
||||
expect(dictDeleted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-005: 创建字典数据', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('点击新增字典数据按钮', async () => {
|
||||
await dictManagementPage.clickCreateDictData();
|
||||
});
|
||||
|
||||
await test.step('填写字典数据信息', async () => {
|
||||
const timestamp = Date.now();
|
||||
const dictData = {
|
||||
dictLabel: `测试字典标签_${timestamp}`,
|
||||
dictValue: `test_value_${timestamp}`,
|
||||
dictType: 'sys_normal_disable',
|
||||
cssClass: 'el-tag-success',
|
||||
listClass: 'default',
|
||||
isDefault: 'Y',
|
||||
status: '1',
|
||||
sort: 1
|
||||
};
|
||||
await dictManagementPage.fillDictDataForm(dictData);
|
||||
});
|
||||
|
||||
await test.step('提交表单', async () => {
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
await test.step('验证字典数据创建成功', async () => {
|
||||
await dictManagementPage.reload();
|
||||
const dictDataCount = await dictManagementPage.getDictDataCount();
|
||||
expect(dictDataCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-006: 编辑字典数据', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建测试字典数据', async () => {
|
||||
await dictManagementPage.clickCreateDictData();
|
||||
const timestamp = Date.now();
|
||||
const dictData = {
|
||||
dictLabel: `待编辑标签_${timestamp}`,
|
||||
dictValue: `edit_value_${timestamp}`,
|
||||
dictType: 'sys_normal_disable',
|
||||
status: '1',
|
||||
sort: 1
|
||||
};
|
||||
await dictManagementPage.fillDictDataForm(dictData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
await test.step('编辑字典数据', async () => {
|
||||
const timestamp = Date.now();
|
||||
await dictManagementPage.editDictData(`待编辑标签_${timestamp}`);
|
||||
await page.waitForTimeout(500);
|
||||
const updateData = {
|
||||
dictLabel: `已编辑标签_${timestamp}`,
|
||||
cssClass: 'el-tag-warning'
|
||||
};
|
||||
await dictManagementPage.fillDictDataForm(updateData);
|
||||
});
|
||||
|
||||
await test.step('提交修改', async () => {
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-007: 删除字典数据', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建测试字典数据', async () => {
|
||||
await dictManagementPage.clickCreateDictData();
|
||||
const timestamp = Date.now();
|
||||
const dictData = {
|
||||
dictLabel: `待删除标签_${timestamp}`,
|
||||
dictValue: `delete_value_${timestamp}`,
|
||||
dictType: 'sys_normal_disable',
|
||||
status: '1',
|
||||
sort: 1
|
||||
};
|
||||
await dictManagementPage.fillDictDataForm(dictData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
await test.step('删除字典数据', async () => {
|
||||
const timestamp = Date.now();
|
||||
await dictManagementPage.deleteDictData(`待删除标签_${timestamp}`);
|
||||
await dictManagementPage.confirmDelete();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
await test.step('验证字典数据已删除', async () => {
|
||||
await dictManagementPage.reload();
|
||||
const timestamp = Date.now();
|
||||
const dictDataDeleted = await dictManagementPage.containsText(`待删除标签_${timestamp}`);
|
||||
expect(dictDataDeleted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-008: 搜索字典', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('搜索字典类型', async () => {
|
||||
await dictManagementPage.search('系统');
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
await test.step('验证搜索结果', async () => {
|
||||
const searchResult = await dictManagementPage.containsText('系统');
|
||||
expect(searchResult).toBe(true);
|
||||
});
|
||||
|
||||
await test.step('清除搜索', async () => {
|
||||
await dictManagementPage.search('');
|
||||
await page.waitForTimeout(1000);
|
||||
const dictTypeCount = await dictManagementPage.getDictTypeCount();
|
||||
expect(dictTypeCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-009: 字典状态管理', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建启用状态的字典类型', async () => {
|
||||
await dictManagementPage.clickCreateDictType();
|
||||
const timestamp = Date.now();
|
||||
const dictTypeData = {
|
||||
dictName: `启用字典_${timestamp}`,
|
||||
dictType: `enabled_dict_${timestamp}`,
|
||||
status: '1',
|
||||
remark: '这是启用的字典'
|
||||
};
|
||||
await dictManagementPage.fillDictTypeForm(dictTypeData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
await test.step('创建禁用状态的字典类型', async () => {
|
||||
await dictManagementPage.clickCreateDictType();
|
||||
const timestamp = Date.now();
|
||||
const dictTypeData = {
|
||||
dictName: `禁用字典_${timestamp}`,
|
||||
dictType: `disabled_dict_${timestamp}`,
|
||||
status: '0',
|
||||
remark: '这是禁用的字典'
|
||||
};
|
||||
await dictManagementPage.fillDictTypeForm(dictTypeData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-010: 字典排序功能', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建多个字典数据测试排序', async () => {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
await dictManagementPage.clickCreateDictData();
|
||||
const timestamp = Date.now();
|
||||
const dictData = {
|
||||
dictLabel: `排序标签_${i}_${timestamp}`,
|
||||
dictValue: `sort_value_${i}_${timestamp}`,
|
||||
dictType: 'sys_normal_disable',
|
||||
status: '1',
|
||||
sort: i
|
||||
};
|
||||
await dictManagementPage.fillDictDataForm(dictData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证字典数据按排序号显示', async () => {
|
||||
await dictManagementPage.reload();
|
||||
await page.waitForTimeout(1000);
|
||||
const dictDataCount = await dictManagementPage.getDictDataCount();
|
||||
expect(dictDataCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-011: 字典默认值设置', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建默认字典数据', async () => {
|
||||
await dictManagementPage.clickCreateDictData();
|
||||
const timestamp = Date.now();
|
||||
const dictData = {
|
||||
dictLabel: `默认标签_${timestamp}`,
|
||||
dictValue: `default_value_${timestamp}`,
|
||||
dictType: 'sys_normal_disable',
|
||||
isDefault: 'Y',
|
||||
status: '1',
|
||||
sort: 1
|
||||
};
|
||||
await dictManagementPage.fillDictDataForm(dictData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
await test.step('创建非默认字典数据', async () => {
|
||||
await dictManagementPage.clickCreateDictData();
|
||||
const timestamp = Date.now();
|
||||
const dictData = {
|
||||
dictLabel: `非默认标签_${timestamp}`,
|
||||
dictValue: `non_default_value_${timestamp}`,
|
||||
dictType: 'sys_normal_disable',
|
||||
isDefault: 'N',
|
||||
status: '1',
|
||||
sort: 2
|
||||
};
|
||||
await dictManagementPage.fillDictDataForm(dictData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-012: 字典CSS样式配置', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建带CSS样式的字典数据', async () => {
|
||||
await dictManagementPage.clickCreateDictData();
|
||||
const timestamp = Date.now();
|
||||
const dictData = {
|
||||
dictLabel: `样式标签_${timestamp}`,
|
||||
dictValue: `style_value_${timestamp}`,
|
||||
dictType: 'sys_normal_disable',
|
||||
cssClass: 'el-tag-success',
|
||||
listClass: 'default',
|
||||
status: '1',
|
||||
sort: 1
|
||||
};
|
||||
await dictManagementPage.fillDictDataForm(dictData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-013: 字典数据验证', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证字典类型数据完整性', async () => {
|
||||
const dictTypeCount = await dictManagementPage.getDictTypeCount();
|
||||
expect(dictTypeCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
await test.step('验证字典数据完整性', async () => {
|
||||
const dictDataCount = await dictManagementPage.getDictDataCount();
|
||||
expect(dictDataCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
await test.step('验证表格包含必要列', async () => {
|
||||
await expect(dictManagementPage.table).toContainText('字典名称');
|
||||
await expect(dictManagementPage.table).toContainText('字典类型');
|
||||
await expect(dictManagementPage.table).toContainText('状态');
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-014: 字典响应式布局', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证桌面端布局', async () => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await expect(dictManagementPage.table).toBeVisible();
|
||||
await expect(dictManagementPage.createDictTypeButton).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证平板端布局', async () => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await expect(dictManagementPage.table).toBeVisible();
|
||||
await expect(dictManagementPage.createDictTypeButton).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证移动端布局', async () => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await expect(dictManagementPage.table).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('DICT-015: 字典类型与数据关联', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await dictManagementPage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建字典类型', async () => {
|
||||
await dictManagementPage.clickCreateDictType();
|
||||
const timestamp = Date.now();
|
||||
const dictTypeData = {
|
||||
dictName: `关联测试字典_${timestamp}`,
|
||||
dictType: `relation_dict_${timestamp}`,
|
||||
status: '1',
|
||||
remark: '用于测试类型与数据关联'
|
||||
};
|
||||
await dictManagementPage.fillDictTypeForm(dictTypeData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
await test.step('为该类型创建多个字典数据', async () => {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
await dictManagementPage.clickCreateDictData();
|
||||
const timestamp = Date.now();
|
||||
const dictData = {
|
||||
dictLabel: `关联数据_${i}_${timestamp}`,
|
||||
dictValue: `relation_value_${i}_${timestamp}`,
|
||||
dictType: `relation_dict_${timestamp}`,
|
||||
status: '1',
|
||||
sort: i
|
||||
};
|
||||
await dictManagementPage.fillDictDataForm(dictData);
|
||||
await dictManagementPage.submitForm();
|
||||
await expect(dictManagementPage.isSuccessMessageVisible()).resolves.toBe(true);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证字典数据关联成功', async () => {
|
||||
await dictManagementPage.reload();
|
||||
const dictDataCount = await dictManagementPage.getDictDataCount();
|
||||
expect(dictDataCount).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -8,8 +8,21 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
let backendProcess: ChildProcess | null = null;
|
||||
let gatewayProcess: ChildProcess | null = null;
|
||||
let healthCheckInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
function renderProgressBar(label: string, current: number, total: number, width: number = 30): void {
|
||||
const ratio = Math.min(current / total, 1);
|
||||
const filled = Math.round(ratio * width);
|
||||
const empty = width - filled;
|
||||
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
||||
const percent = (ratio * 100).toFixed(0);
|
||||
process.stdout.write(`\r ${label} [${bar}] ${percent}% (${current}/${total}s)`);
|
||||
if (ratio >= 1) {
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkBackendHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8084/actuator/health', {
|
||||
@@ -25,16 +38,51 @@ async function checkBackendHealth(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGatewayHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8080/actuator/health', {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
} as any);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data.status === 'UP';
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkFrontendHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('http://localhost:3002', {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
} as any);
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function startHealthMonitoring() {
|
||||
if (healthCheckInterval) {
|
||||
clearInterval(healthCheckInterval);
|
||||
}
|
||||
|
||||
|
||||
healthCheckInterval = setInterval(async () => {
|
||||
const isHealthy = await checkBackendHealth();
|
||||
if (!isHealthy) {
|
||||
const backendHealthy = await checkBackendHealth();
|
||||
const gatewayHealthy = await checkGatewayHealth();
|
||||
const frontendHealthy = await checkFrontendHealth();
|
||||
|
||||
if (!backendHealthy) {
|
||||
console.error('⚠️ 后端服务健康检查失败!');
|
||||
}
|
||||
if (!gatewayHealthy) {
|
||||
console.error('⚠️ 网关服务健康检查失败!');
|
||||
}
|
||||
if (!frontendHealthy) {
|
||||
console.error('⚠️ 前端服务健康检查失败!');
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
@@ -47,147 +95,365 @@ function stopHealthMonitoring() {
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
console.log('🚀 开始全局测试环境设置...');
|
||||
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.PLAYWRIGHT_HEADLESS = 'false';
|
||||
|
||||
const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app');
|
||||
const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar');
|
||||
|
||||
let backendCommand: string;
|
||||
let backendArgs: string[];
|
||||
|
||||
if (existsSync(jarFile)) {
|
||||
console.log('📦 使用JAR文件启动后端服务...');
|
||||
console.log(` JAR文件: ${jarFile}`);
|
||||
backendCommand = 'java';
|
||||
backendArgs = [
|
||||
'-jar',
|
||||
jarFile,
|
||||
'--spring.profiles.active=test',
|
||||
'-Xms256m',
|
||||
'-Xmx512m'
|
||||
];
|
||||
|
||||
const backendAlreadyRunning = await checkBackendHealth();
|
||||
if (backendAlreadyRunning) {
|
||||
console.log('✅ 后端服务已在运行,跳过启动');
|
||||
} else {
|
||||
console.log('📦 使用Maven启动后端服务...');
|
||||
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
|
||||
backendCommand = 'mvn';
|
||||
backendArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=test'];
|
||||
}
|
||||
|
||||
console.log(` 目录: ${backendDir}`);
|
||||
console.log(` 命令: ${backendCommand} ${backendArgs.join(' ')}`);
|
||||
|
||||
backendProcess = spawn(backendCommand, backendArgs, {
|
||||
cwd: backendDir,
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
detached: false,
|
||||
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'test' }
|
||||
});
|
||||
|
||||
if (backendProcess.stdout) {
|
||||
backendProcess.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
if (output.includes('Started ManageApplication') || output.includes('Tomcat started on port')) {
|
||||
console.log('✅ 后端服务启动成功');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (backendProcess.stderr) {
|
||||
backendProcess.stderr.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
if (output.includes('ERROR') || output.includes('Exception')) {
|
||||
console.error('❌ 后端服务启动错误:', output);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
backendProcess.on('error', (error) => {
|
||||
console.error('❌ 后端服务启动失败:', error);
|
||||
});
|
||||
|
||||
backendProcess.on('exit', (code, signal) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
console.error(`❌ 后端服务异常退出,退出码: ${code}, 信号: ${signal}`);
|
||||
const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app');
|
||||
const jarFile = path.join(backendDir, 'target/manage-app-1.0.0.jar');
|
||||
|
||||
let backendCommand: string;
|
||||
let backendArgs: string[];
|
||||
|
||||
if (existsSync(jarFile)) {
|
||||
console.log('📦 使用JAR文件启动后端服务...');
|
||||
console.log(` JAR文件: ${jarFile}`);
|
||||
backendCommand = 'java';
|
||||
backendArgs = [
|
||||
'-jar',
|
||||
jarFile,
|
||||
'--spring.profiles.active=test',
|
||||
'-Xms256m',
|
||||
'-Xmx512m'
|
||||
];
|
||||
} else {
|
||||
console.log('📦 使用Maven启动后端服务...');
|
||||
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
|
||||
backendCommand = 'mvn';
|
||||
backendArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=test'];
|
||||
}
|
||||
});
|
||||
|
||||
console.log('⏳ 等待后端服务就绪...');
|
||||
await waitForBackendReady();
|
||||
|
||||
|
||||
console.log(` 目录: ${backendDir}`);
|
||||
console.log(` 命令: ${backendCommand} ${backendArgs.join(' ')}`);
|
||||
|
||||
backendProcess = spawn(backendCommand, backendArgs, {
|
||||
cwd: backendDir,
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
detached: false,
|
||||
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'test' }
|
||||
});
|
||||
|
||||
if (backendProcess.stdout) {
|
||||
backendProcess.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
if (output.includes('Started ManageApplication') || output.includes('Tomcat started on port')) {
|
||||
console.log('✅ 后端服务启动成功');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (backendProcess.stderr) {
|
||||
backendProcess.stderr.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
if (output.includes('ERROR') || output.includes('Exception')) {
|
||||
console.error('❌ 后端服务启动错误:', output);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
backendProcess.on('error', (error) => {
|
||||
console.error('❌ 后端服务启动失败:', error);
|
||||
});
|
||||
|
||||
backendProcess.on('exit', (code, signal) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
console.error(`❌ 后端服务异常退出,退出码: ${code}, 信号: ${signal}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('⏳ 等待后端服务就绪...');
|
||||
await waitForBackendReady();
|
||||
}
|
||||
|
||||
const gatewayAlreadyRunning = await checkGatewayHealth();
|
||||
if (gatewayAlreadyRunning) {
|
||||
console.log('✅ 网关服务已在运行,跳过启动');
|
||||
} else {
|
||||
const gatewayDir = path.resolve(__dirname, '../../novalon-manage-api/manage-gateway');
|
||||
const gatewayJarFile = path.join(gatewayDir, 'target/manage-gateway-1.0.0.jar');
|
||||
|
||||
let gatewayCommand: string;
|
||||
let gatewayArgs: string[];
|
||||
|
||||
if (existsSync(gatewayJarFile)) {
|
||||
console.log('🚪 使用JAR文件启动网关服务...');
|
||||
console.log(` JAR文件: ${gatewayJarFile}`);
|
||||
gatewayCommand = 'java';
|
||||
gatewayArgs = [
|
||||
'-jar',
|
||||
gatewayJarFile,
|
||||
'--spring.profiles.active=dev',
|
||||
'-Xms128m',
|
||||
'-Xmx256m'
|
||||
];
|
||||
} else {
|
||||
console.log('🚪 使用Maven启动网关服务...');
|
||||
console.log(' 提示: 运行 "mvn clean package -DskipTests" 构建JAR文件以获得更快的启动速度');
|
||||
gatewayCommand = 'mvn';
|
||||
gatewayArgs = ['spring-boot:run', '-Dspring-boot.run.profiles=dev'];
|
||||
}
|
||||
|
||||
console.log(` 目录: ${gatewayDir}`);
|
||||
console.log(` 命令: ${gatewayCommand} ${gatewayArgs.join(' ')}`);
|
||||
|
||||
gatewayProcess = spawn(gatewayCommand, gatewayArgs, {
|
||||
cwd: gatewayDir,
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
detached: false,
|
||||
env: { ...process.env, SPRING_PROFILES_ACTIVE: 'dev' }
|
||||
});
|
||||
|
||||
if (gatewayProcess.stdout) {
|
||||
gatewayProcess.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
if (output.includes('Started GatewayApplication') || output.includes('Netty started on port')) {
|
||||
console.log('✅ 网关服务启动成功');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (gatewayProcess.stderr) {
|
||||
gatewayProcess.stderr.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
if (output.includes('ERROR') || output.includes('Exception')) {
|
||||
console.error('❌ 网关服务启动错误:', output);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
gatewayProcess.on('error', (error) => {
|
||||
console.error('❌ 网关服务启动失败:', error);
|
||||
});
|
||||
|
||||
gatewayProcess.on('exit', (code, signal) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
console.error(`❌ 网关服务异常退出,退出码: ${code}, 信号: ${signal}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('⏳ 等待网关服务就绪...');
|
||||
await waitForGatewayReady();
|
||||
}
|
||||
|
||||
console.log('🔍 验证所有服务连通性...');
|
||||
await verifyAllServices();
|
||||
|
||||
console.log('🧹 清理测试数据...');
|
||||
await cleanupTestData();
|
||||
|
||||
|
||||
startHealthMonitoring();
|
||||
|
||||
|
||||
console.log('✅ 全局测试环境设置完成');
|
||||
}
|
||||
|
||||
async function verifyAllServices(): Promise<void> {
|
||||
console.log(' 验证后端服务...');
|
||||
const backendOk = await checkBackendHealth();
|
||||
if (!backendOk) {
|
||||
throw new Error('❌ 后端服务验证失败');
|
||||
}
|
||||
console.log(' ✅ 后端服务正常');
|
||||
|
||||
console.log(' 验证网关服务...');
|
||||
const gatewayOk = await checkGatewayHealth();
|
||||
if (!gatewayOk) {
|
||||
throw new Error('❌ 网关服务验证失败');
|
||||
}
|
||||
console.log(' ✅ 网关服务正常');
|
||||
|
||||
console.log(' 验证网关到后端的连通性...');
|
||||
try {
|
||||
const response = await fetch('http://localhost:8080/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
|
||||
signal: AbortSignal.timeout(10000) as any
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`网关到后端连通性验证失败,状态码: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.token) {
|
||||
throw new Error('网关到后端连通性验证失败,未返回token');
|
||||
}
|
||||
|
||||
console.log(' ✅ 网关到后端连通性正常');
|
||||
} catch (error) {
|
||||
throw new Error(`❌ 网关到后端连通性验证失败: ${error}`);
|
||||
}
|
||||
|
||||
console.log('✅ 所有服务验证通过');
|
||||
}
|
||||
|
||||
async function waitForBackendReady(): Promise<void> {
|
||||
const maxRetries = 60;
|
||||
const maxRetries = 90;
|
||||
const retryInterval = 1000;
|
||||
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
renderProgressBar('⏳ 后端服务启动中', i, maxRetries);
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8084/actuator/health');
|
||||
const response = await fetch('http://localhost:8084/actuator/health', {
|
||||
signal: AbortSignal.timeout(5000) as any
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.status === 'UP') {
|
||||
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
||||
console.log(`✅ 后端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
|
||||
return;
|
||||
|
||||
try {
|
||||
const loginTest = await fetch('http://localhost:8084/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
|
||||
signal: AbortSignal.timeout(10000) as any
|
||||
});
|
||||
|
||||
if (loginTest.ok) {
|
||||
console.log('✅ 后端服务连通性验证通过(登录API可用)');
|
||||
return;
|
||||
} else {
|
||||
console.log(`⚠️ 后端服务连通性验证失败,状态码: ${loginTest.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ 后端服务连通性验证失败,继续等待...');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 服务还未就绪,继续等待
|
||||
}
|
||||
|
||||
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
throw new Error('❌ 后端服务启动超时');
|
||||
}
|
||||
|
||||
async function waitForGatewayReady(): Promise<void> {
|
||||
const maxRetries = 90;
|
||||
const retryInterval = 1000;
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
renderProgressBar('⏳ 网关服务启动中', i, maxRetries);
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8080/actuator/health', {
|
||||
signal: AbortSignal.timeout(5000) as any
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.status === 'UP') {
|
||||
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
||||
console.log(`✅ 网关服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
|
||||
|
||||
try {
|
||||
const loginTest = await fetch('http://localhost:8080/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'Test@123' }),
|
||||
signal: AbortSignal.timeout(10000) as any
|
||||
});
|
||||
|
||||
if (loginTest.ok) {
|
||||
console.log('✅ 网关服务连通性验证通过(登录API可用)');
|
||||
return;
|
||||
} else {
|
||||
console.log(`⚠️ 网关服务连通性验证失败,状态码: ${loginTest.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ 网关服务连通性验证失败,继续等待...');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 服务还未就绪,继续等待
|
||||
}
|
||||
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('❌ 网关服务启动超时');
|
||||
}
|
||||
|
||||
async function waitForFrontendReady(): Promise<void> {
|
||||
const maxRetries = 90;
|
||||
const retryInterval = 1000;
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
renderProgressBar('⏳ 前端服务启动中', i, maxRetries);
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3002', {
|
||||
signal: AbortSignal.timeout(5000) as any
|
||||
});
|
||||
if (response.ok) {
|
||||
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
||||
console.log(`✅ 前端服务健康检查通过 (尝试 ${i + 1}/${maxRetries})`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// 服务还未就绪,继续等待
|
||||
}
|
||||
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('❌ 前端服务启动超时');
|
||||
}
|
||||
|
||||
async function cleanupTestData(): Promise<void> {
|
||||
try {
|
||||
// 登录获取token
|
||||
const loginResponse = await fetch('http://localhost:8084/api/auth/login', {
|
||||
// 登录获取token(通过网关)
|
||||
const loginResponse = await fetch('http://localhost:8080/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
password: 'Test@123'
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
console.log('⚠️ 无法登录,跳过数据清理');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
const token = loginData.token;
|
||||
|
||||
|
||||
// 获取所有用户
|
||||
const usersResponse = await fetch('http://localhost:8084/api/users', {
|
||||
const usersResponse = await fetch('http://localhost:8080/api/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (usersResponse.ok) {
|
||||
const users = await usersResponse.json();
|
||||
|
||||
|
||||
// 删除测试创建的用户(保留ID 1-10的初始用户)
|
||||
for (const user of users) {
|
||||
if (user.id > 10) {
|
||||
try {
|
||||
await fetch(`http://localhost:8084/api/users/${user.id}`, {
|
||||
await fetch(`http://localhost:8080/api/users/${user.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
@@ -200,22 +466,22 @@ async function cleanupTestData(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取所有角色
|
||||
const rolesResponse = await fetch('http://localhost:8084/api/roles', {
|
||||
const rolesResponse = await fetch('http://localhost:8080/api/roles', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (rolesResponse.ok) {
|
||||
const roles = await rolesResponse.json();
|
||||
|
||||
|
||||
// 删除测试创建的角色(保留ID 1-4的初始角色)
|
||||
for (const role of roles) {
|
||||
if (role.id > 4) {
|
||||
try {
|
||||
await fetch(`http://localhost:8084/api/roles/${role.id}`, {
|
||||
await fetch(`http://localhost:8080/api/roles/${role.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
@@ -228,7 +494,7 @@ async function cleanupTestData(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log('✅ 测试数据清理完成');
|
||||
} catch (error) {
|
||||
console.log('⚠️ 数据清理失败,继续执行测试');
|
||||
@@ -238,20 +504,20 @@ async function cleanupTestData(): Promise<void> {
|
||||
|
||||
async function globalTeardown() {
|
||||
console.log('🧹 开始全局测试环境清理...');
|
||||
|
||||
|
||||
stopHealthMonitoring();
|
||||
|
||||
|
||||
if (backendProcess) {
|
||||
console.log('🛑 停止后端服务...');
|
||||
backendProcess.kill('SIGTERM');
|
||||
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
if (backendProcess) {
|
||||
backendProcess.on('exit', () => {
|
||||
console.log('✅ 后端服务已停止');
|
||||
resolve();
|
||||
});
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
if (backendProcess) {
|
||||
backendProcess.kill('SIGKILL');
|
||||
@@ -264,7 +530,31 @@ async function globalTeardown() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (gatewayProcess) {
|
||||
console.log('🛑 停止网关服务...');
|
||||
gatewayProcess.kill('SIGTERM');
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
if (gatewayProcess) {
|
||||
gatewayProcess.on('exit', () => {
|
||||
console.log('✅ 网关服务已停止');
|
||||
resolve();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (gatewayProcess) {
|
||||
gatewayProcess.kill('SIGKILL');
|
||||
console.log('⚠️ 强制停止网关服务');
|
||||
resolve();
|
||||
}
|
||||
}, 10000);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ 全局测试环境清理完成');
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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`);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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('排序');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,195 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { NotificationPage } from './pages/NotificationPage';
|
||||
|
||||
test.describe('通知公告E2E测试', () => {
|
||||
let loginPage: LoginPage;
|
||||
let noticePage: NotificationPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
noticePage = new NotificationPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await loginPage.logout();
|
||||
});
|
||||
|
||||
test('通知公告页面导航', async ({ page }) => {
|
||||
await test.step('导航到通知公告页面', async () => {
|
||||
await noticePage.goto();
|
||||
await expect(page).toHaveURL(/.*notice/);
|
||||
});
|
||||
|
||||
await test.step('验证页面元素可见', async () => {
|
||||
await expect(noticePage.table).toBeVisible();
|
||||
await expect(noticePage.addButton).toBeVisible();
|
||||
await expect(noticePage.searchInput).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('创建通知公告', async ({ page }) => {
|
||||
await test.step('导航到通知公告页面', async () => {
|
||||
await noticePage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建新通知公告', async () => {
|
||||
const title = `测试通知_${Date.now()}`;
|
||||
const content = `这是一条测试通知内容_${Date.now()}`;
|
||||
|
||||
await noticePage.addNotification(title, content);
|
||||
|
||||
await noticePage.verifyTableContains(title);
|
||||
});
|
||||
});
|
||||
|
||||
test('编辑通知公告', async ({ page }) => {
|
||||
await test.step('导航到通知公告页面', async () => {
|
||||
await noticePage.goto();
|
||||
});
|
||||
|
||||
await test.step('编辑现有通知公告', async () => {
|
||||
const title = '系统维护通知';
|
||||
const newContent = `系统将于今晚进行维护,请提前保存工作_${Date.now()}`;
|
||||
|
||||
await noticePage.editNotification(title, newContent);
|
||||
|
||||
await noticePage.verifyTableContains(title);
|
||||
});
|
||||
});
|
||||
|
||||
test('删除通知公告', async ({ page }) => {
|
||||
await test.step('导航到通知公告页面', async () => {
|
||||
await noticePage.goto();
|
||||
});
|
||||
|
||||
await test.step('删除通知公告', async () => {
|
||||
const title = `测试通知_${Date.now()}`;
|
||||
const content = `这是一条测试通知内容_${Date.now()}`;
|
||||
|
||||
await noticePage.addNotification(title, content);
|
||||
await noticePage.verifyTableContains(title);
|
||||
|
||||
await noticePage.deleteNotification(title);
|
||||
await noticePage.verifyTableNotContains(title);
|
||||
});
|
||||
});
|
||||
|
||||
test('搜索通知公告', async ({ page }) => {
|
||||
await test.step('导航到通知公告页面', async () => {
|
||||
await noticePage.goto();
|
||||
});
|
||||
|
||||
await test.step('搜索通知公告', async () => {
|
||||
const title = '系统维护通知';
|
||||
|
||||
await noticePage.searchNotification(title);
|
||||
await noticePage.verifyTableContains(title);
|
||||
});
|
||||
|
||||
await test.step('清除搜索', async () => {
|
||||
await noticePage.clearSearch();
|
||||
const rowCount = await noticePage.getTableRowCount();
|
||||
expect(rowCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('通知公告分页功能', async ({ page }) => {
|
||||
await test.step('导航到通知公告页面', async () => {
|
||||
await noticePage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证表格数据加载', async () => {
|
||||
const rowCount = await noticePage.getTableRowCount();
|
||||
expect(rowCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('通知公告响应式布局', async ({ page }) => {
|
||||
await test.step('导航到通知公告页面', async () => {
|
||||
await noticePage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证桌面端布局', async () => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await expect(noticePage.table).toBeVisible();
|
||||
await expect(noticePage.addButton).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证平板端布局', async () => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await expect(noticePage.table).toBeVisible();
|
||||
await expect(noticePage.addButton).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证移动端布局', async () => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await expect(noticePage.table).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('通知公告权限验证', async ({ page }) => {
|
||||
await test.step('导航到通知公告页面', async () => {
|
||||
await noticePage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证添加按钮可见性', async () => {
|
||||
await expect(noticePage.addButton).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证编辑和删除按钮可见性', async () => {
|
||||
const rows = await noticePage.table.locator('.el-table__row').count();
|
||||
if (rows > 0) {
|
||||
await expect(noticePage.table).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('通知公告状态管理', async ({ page }) => {
|
||||
await test.step('导航到通知公告页面', async () => {
|
||||
await noticePage.goto();
|
||||
});
|
||||
|
||||
await test.step('创建已发布通知', async () => {
|
||||
const title = `已发布通知_${Date.now()}`;
|
||||
const content = `这是一条已发布的通知_${Date.now()}`;
|
||||
|
||||
await noticePage.addNotification(title, content, '1', '0');
|
||||
await noticePage.verifyTableContains(title);
|
||||
});
|
||||
|
||||
await test.step('创建草稿通知', async () => {
|
||||
const title = `草稿通知_${Date.now()}`;
|
||||
const content = `这是一条草稿通知_${Date.now()}`;
|
||||
|
||||
await noticePage.addNotification(title, content, '1', '1');
|
||||
await noticePage.verifyTableContains(title);
|
||||
});
|
||||
});
|
||||
|
||||
test('通知公告内容验证', async ({ page }) => {
|
||||
await test.step('导航到通知公告页面', async () => {
|
||||
await noticePage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证通知标题长度限制', async () => {
|
||||
const longTitle = '这是一个非常非常长的通知标题,用于测试系统对长标题的处理能力,确保系统能够正确显示和存储长标题';
|
||||
const content = '测试内容';
|
||||
|
||||
await noticePage.addNotification(longTitle, content);
|
||||
await noticePage.verifyTableContains(longTitle.substring(0, 50));
|
||||
});
|
||||
|
||||
await test.step('验证通知内容格式', async () => {
|
||||
const title = `格式测试通知_${Date.now()}`;
|
||||
const content = '支持富文本格式:<b>粗体</b>、<i>斜体</i>、<u>下划线</u>';
|
||||
|
||||
await noticePage.addNotification(title, content);
|
||||
await noticePage.verifyTableContains(title);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,192 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { OperationLogPage } from './pages/OperationLogPage';
|
||||
|
||||
test.describe('操作日志E2E测试', () => {
|
||||
let loginPage: LoginPage;
|
||||
let operationLogPage: OperationLogPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
operationLogPage = new OperationLogPage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
await loginPage.login('admin', 'admin123');
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await loginPage.logout();
|
||||
});
|
||||
|
||||
test('操作日志页面导航', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
await expect(page).toHaveURL(/.*oplog/);
|
||||
});
|
||||
|
||||
await test.step('验证页面元素可见', async () => {
|
||||
await expect(operationLogPage.table).toBeVisible();
|
||||
await expect(operationLogPage.searchInput).toBeVisible();
|
||||
await expect(operationLogPage.exportButton).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('搜索操作日志', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
});
|
||||
|
||||
await test.step('搜索操作日志', async () => {
|
||||
const keyword = 'admin';
|
||||
|
||||
await operationLogPage.searchByKeyword(keyword);
|
||||
await operationLogPage.verifyTableContains(keyword);
|
||||
});
|
||||
|
||||
await test.step('清除搜索', async () => {
|
||||
await operationLogPage.clearSearch();
|
||||
const rowCount = await operationLogPage.getTableRowCount();
|
||||
expect(rowCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('操作日志分页功能', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证表格数据加载', async () => {
|
||||
const rowCount = await operationLogPage.getTableRowCount();
|
||||
expect(rowCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('操作日志响应式布局', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证桌面端布局', async () => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await expect(operationLogPage.table).toBeVisible();
|
||||
await expect(operationLogPage.exportButton).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证平板端布局', async () => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await expect(operationLogPage.table).toBeVisible();
|
||||
await expect(operationLogPage.exportButton).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证移动端布局', async () => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await expect(operationLogPage.table).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('操作日志数据验证', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证日志数据完整性', async () => {
|
||||
const rowCount = await operationLogPage.getTableRowCount();
|
||||
expect(rowCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
await test.step('验证日志字段显示', async () => {
|
||||
await expect(operationLogPage.table).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('操作日志搜索功能', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
});
|
||||
|
||||
await test.step('按操作人搜索', async () => {
|
||||
const operator = 'admin';
|
||||
await operationLogPage.searchByKeyword(operator);
|
||||
await operationLogPage.verifyTableContains(operator);
|
||||
});
|
||||
|
||||
await test.step('按操作模块搜索', async () => {
|
||||
const module = '用户管理';
|
||||
await operationLogPage.searchByKeyword(module);
|
||||
});
|
||||
|
||||
await test.step('清除搜索结果', async () => {
|
||||
await operationLogPage.clearSearch();
|
||||
const rowCount = await operationLogPage.getTableRowCount();
|
||||
expect(rowCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('操作日志导出功能', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
});
|
||||
|
||||
await test.step('导出操作日志', async () => {
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await operationLogPage.exportData();
|
||||
const download = await downloadPromise;
|
||||
expect(download).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test('操作日志时间范围验证', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证日志时间戳显示', async () => {
|
||||
const rowCount = await operationLogPage.getTableRowCount();
|
||||
if (rowCount > 0) {
|
||||
await expect(operationLogPage.table).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('操作日志权限验证', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证导出按钮可见性', async () => {
|
||||
await expect(operationLogPage.exportButton).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证搜索功能可用', async () => {
|
||||
await expect(operationLogPage.searchInput).toBeVisible();
|
||||
await expect(operationLogPage.searchButton).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('操作日志详情查看', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证日志详情显示', async () => {
|
||||
const rowCount = await operationLogPage.getTableRowCount();
|
||||
if (rowCount > 0) {
|
||||
await expect(operationLogPage.table).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('操作日志排序功能', async ({ page }) => {
|
||||
await test.step('导航到操作日志页面', async () => {
|
||||
await operationLogPage.goto();
|
||||
});
|
||||
|
||||
await test.step('验证表格排序功能', async () => {
|
||||
const rowCount = await operationLogPage.getTableRowCount();
|
||||
if (rowCount > 0) {
|
||||
await expect(operationLogPage.table).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,24 +3,24 @@ import { Page, Locator, expect } from '@playwright/test';
|
||||
export class DictionaryManagementPage {
|
||||
readonly page: Page;
|
||||
readonly table: Locator;
|
||||
readonly createDictTypeButton: Locator;
|
||||
readonly createDictDataButton: Locator;
|
||||
readonly searchInput: Locator;
|
||||
readonly searchButton: Locator;
|
||||
readonly successMessage: Locator;
|
||||
readonly dictTypeTable: Locator;
|
||||
readonly dictDataTable: Locator;
|
||||
readonly createDictButton: Locator;
|
||||
readonly saveButton: Locator;
|
||||
readonly dialog: Locator;
|
||||
readonly dictNameInput: Locator;
|
||||
readonly dictTypeInput: Locator;
|
||||
readonly statusSelect: Locator;
|
||||
readonly remarkInput: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.locator('.el-table').or(page.locator('.dict-table'));
|
||||
this.createDictTypeButton = page.getByRole('button', { name: '新增字典类型' }).or(page.locator('button:has-text("新增字典类型")'));
|
||||
this.createDictDataButton = page.getByRole('button', { name: '新增字典数据' }).or(page.locator('button:has-text("新增字典数据")'));
|
||||
this.searchInput = page.locator('input[placeholder*="搜索"]').or(page.locator('input[name*="keyword"]'));
|
||||
this.searchButton = page.getByRole('button', { name: '搜索' }).or(page.locator('button:has-text("搜索")'));
|
||||
this.successMessage = page.locator('.el-message--success').or(page.locator('.success-message'));
|
||||
this.dictTypeTable = page.locator('.dict-type-table').or(page.locator('.el-table').first());
|
||||
this.dictDataTable = page.locator('.dict-data-table').or(page.locator('.el-table').nth(1));
|
||||
this.table = page.locator('.el-table');
|
||||
this.createDictButton = page.getByRole('button', { name: '新增字典' });
|
||||
this.saveButton = page.getByRole('button', { name: '确定' });
|
||||
this.dialog = page.locator('.el-dialog');
|
||||
this.dictNameInput = page.locator('.el-dialog').getByRole('textbox', { name: '字典名称' });
|
||||
this.dictTypeInput = page.locator('.el-dialog').getByRole('textbox', { name: '字典类型' });
|
||||
this.statusSelect = page.locator('.el-dialog').getByRole('combobox', { name: '状态' });
|
||||
this.remarkInput = page.locator('.el-dialog').getByRole('textbox', { name: '备注' });
|
||||
}
|
||||
|
||||
async goto() {
|
||||
@@ -40,156 +40,57 @@ export class DictionaryManagementPage {
|
||||
}
|
||||
}
|
||||
|
||||
async clickCreateDictType() {
|
||||
await this.createDictTypeButton.click();
|
||||
async createDict(dictName: string, dictType: string, status: string = '0', remark?: string) {
|
||||
await this.createDictButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.dictNameInput.fill(dictName);
|
||||
await this.dictTypeInput.fill(dictType);
|
||||
|
||||
if (status) {
|
||||
await this.statusSelect.click();
|
||||
await this.page.waitForTimeout(300);
|
||||
await this.page.getByRole('option', { name: status === '0' ? '正常' : '停用' }).click();
|
||||
}
|
||||
|
||||
if (remark) {
|
||||
await this.remarkInput.fill(remark);
|
||||
}
|
||||
|
||||
await this.saveButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async clickCreateDictData() {
|
||||
await this.createDictDataButton.click();
|
||||
async editDict(dictName: string, newDictName: string) {
|
||||
const row = this.table.locator('tr').filter({ hasText: dictName }).first();
|
||||
const editBtn = row.getByRole('button', { name: '编辑' });
|
||||
await editBtn.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.dictNameInput.clear();
|
||||
await this.dictNameInput.fill(newDictName);
|
||||
|
||||
await this.saveButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async fillDictTypeForm(dictTypeData: {
|
||||
dictName: string;
|
||||
dictType: string;
|
||||
status?: string;
|
||||
remark?: string;
|
||||
}) {
|
||||
const dialog = this.page.locator('.el-dialog');
|
||||
async deleteDict(dictName: string) {
|
||||
const row = this.table.locator('tr').filter({ hasText: dictName }).first();
|
||||
const deleteBtn = row.getByRole('button', { name: '删除' });
|
||||
await deleteBtn.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await dialog.locator('input').first().fill(dictTypeData.dictName);
|
||||
await dialog.locator('input').nth(1).fill(dictTypeData.dictType);
|
||||
|
||||
if (dictTypeData.status) {
|
||||
const statusRadio = dialog.locator(`input[value="${dictTypeData.status}"]`);
|
||||
if (await statusRadio.count() > 0) {
|
||||
await statusRadio.check();
|
||||
}
|
||||
}
|
||||
|
||||
if (dictTypeData.remark) {
|
||||
const remarkInput = dialog.locator('textarea');
|
||||
if (await remarkInput.count() > 0) {
|
||||
await remarkInput.fill(dictTypeData.remark);
|
||||
}
|
||||
}
|
||||
const confirmBtn = this.page.locator('.el-message-box').getByRole('button', { name: '确定' });
|
||||
await confirmBtn.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async fillDictDataForm(dictData: {
|
||||
dictLabel: string;
|
||||
dictValue: string;
|
||||
dictType?: string;
|
||||
cssClass?: string;
|
||||
listClass?: string;
|
||||
isDefault?: string;
|
||||
status?: string;
|
||||
sort?: number;
|
||||
}) {
|
||||
const dialog = this.page.locator('.el-dialog');
|
||||
|
||||
await dialog.locator('input').first().fill(dictData.dictLabel);
|
||||
await dialog.locator('input').nth(1).fill(dictData.dictValue);
|
||||
|
||||
if (dictData.dictType) {
|
||||
const dictTypeSelect = dialog.locator('.el-select');
|
||||
if (await dictTypeSelect.count() > 0) {
|
||||
await dictTypeSelect.click();
|
||||
await this.page.waitForTimeout(300);
|
||||
await this.page.getByRole('option', { name: dictData.dictType }).click();
|
||||
}
|
||||
}
|
||||
|
||||
if (dictData.cssClass) {
|
||||
const cssClassInput = dialog.locator('input[placeholder*="CSS"]');
|
||||
if (await cssClassInput.count() > 0) {
|
||||
await cssClassInput.fill(dictData.cssClass);
|
||||
}
|
||||
}
|
||||
|
||||
if (dictData.listClass) {
|
||||
const listClassInput = dialog.locator('input[placeholder*="列表"]');
|
||||
if (await listClassInput.count() > 0) {
|
||||
await listClassInput.fill(dictData.listClass);
|
||||
}
|
||||
}
|
||||
|
||||
if (dictData.isDefault) {
|
||||
const defaultRadio = dialog.locator(`input[value="${dictData.isDefault}"]`);
|
||||
if (await defaultRadio.count() > 0) {
|
||||
await defaultRadio.check();
|
||||
}
|
||||
}
|
||||
|
||||
if (dictData.status) {
|
||||
const statusRadio = dialog.locator(`input[value="${dictData.status}"]`);
|
||||
if (await statusRadio.count() > 0) {
|
||||
await statusRadio.check();
|
||||
}
|
||||
}
|
||||
|
||||
if (dictData.sort !== undefined) {
|
||||
const sortInput = dialog.locator('input[type="number"]');
|
||||
if (await sortInput.count() > 0) {
|
||||
await sortInput.fill(String(dictData.sort));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async submitForm() {
|
||||
await this.page.getByRole('button', { name: '确定' }).or(this.page.locator('button:has-text("确定")')).click();
|
||||
}
|
||||
|
||||
async editDictType(dictName: string) {
|
||||
const dictTypeRow = this.dictTypeTable.locator('tbody tr').filter({ hasText: dictName });
|
||||
await dictTypeRow.getByRole('button', { name: '编辑' }).or(this.page.locator('.edit-button')).click();
|
||||
}
|
||||
|
||||
async editDictData(dictLabel: string) {
|
||||
const dictDataRow = this.dictDataTable.locator('tbody tr').filter({ hasText: dictLabel });
|
||||
await dictDataRow.getByRole('button', { name: '编辑' }).or(this.page.locator('.edit-button')).click();
|
||||
}
|
||||
|
||||
async deleteDictType(dictName: string) {
|
||||
const dictTypeRow = this.dictTypeTable.locator('tbody tr').filter({ hasText: dictName });
|
||||
await dictTypeRow.getByRole('button', { name: '删除' }).or(this.page.locator('.delete-button')).click();
|
||||
}
|
||||
|
||||
async deleteDictData(dictLabel: string) {
|
||||
const dictDataRow = this.dictDataTable.locator('tbody tr').filter({ hasText: dictLabel });
|
||||
await dictDataRow.getByRole('button', { name: '删除' }).or(this.page.locator('.delete-button')).click();
|
||||
}
|
||||
|
||||
async confirmDelete() {
|
||||
await this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.confirm-dialog .confirm-button')).click();
|
||||
}
|
||||
|
||||
async search(keyword: string) {
|
||||
await this.searchInput.fill(keyword);
|
||||
await this.searchButton.click();
|
||||
async getDictCount() {
|
||||
const rows = await this.table.locator('.el-table__row').count();
|
||||
return rows;
|
||||
}
|
||||
|
||||
async containsText(text: string): Promise<boolean> {
|
||||
return await this.table.getByText(text).count() > 0;
|
||||
}
|
||||
|
||||
async isSuccessMessageVisible(): Promise<boolean> {
|
||||
try {
|
||||
return await this.successMessage.isVisible({ timeout: 3000 });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getDictTypeCount(): Promise<number> {
|
||||
return await this.dictTypeTable.locator('tbody tr').count();
|
||||
}
|
||||
|
||||
async getDictDataCount(): Promise<number> {
|
||||
return await this.dictDataTable.locator('tbody tr').count();
|
||||
}
|
||||
|
||||
async reload() {
|
||||
await this.page.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,89 +4,85 @@ export class NotificationPage {
|
||||
readonly page: Page;
|
||||
readonly table;
|
||||
readonly addButton;
|
||||
readonly editButton;
|
||||
readonly deleteButton;
|
||||
readonly saveButton;
|
||||
readonly cancelButton;
|
||||
readonly searchInput;
|
||||
readonly searchButton;
|
||||
readonly dialog;
|
||||
readonly titleInput;
|
||||
readonly contentInput;
|
||||
readonly typeSelect;
|
||||
readonly noticeTypeSelect;
|
||||
readonly statusSelect;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.locator('.el-table');
|
||||
this.addButton = page.getByRole('button', { name: '新增' });
|
||||
this.editButton = page.getByRole('button', { name: '修改' });
|
||||
this.deleteButton = page.getByRole('button', { name: '删除' });
|
||||
this.addButton = page.getByRole('button', { name: '新增公告' });
|
||||
this.saveButton = page.getByRole('button', { name: '确定' });
|
||||
this.cancelButton = page.getByRole('button', { name: '取消' });
|
||||
this.searchInput = page.getByPlaceholder('搜索通知标题');
|
||||
this.searchButton = page.getByRole('button', { name: '搜索' });
|
||||
this.titleInput = page.getByPlaceholder('请输入通知标题');
|
||||
this.contentInput = page.getByPlaceholder('请输入通知内容');
|
||||
this.typeSelect = page.locator('.el-select');
|
||||
this.statusSelect = page.locator('.el-select');
|
||||
this.dialog = page.locator('.el-dialog');
|
||||
this.titleInput = page.locator('.el-dialog').getByRole('textbox', { name: '公告标题' });
|
||||
this.contentInput = page.locator('.el-dialog').getByRole('textbox', { name: '公告内容' });
|
||||
this.noticeTypeSelect = page.locator('.el-dialog').getByRole('combobox', { name: '公告类型' });
|
||||
this.statusSelect = page.locator('.el-dialog').getByRole('combobox', { name: '状态' });
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/system/notice');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
try {
|
||||
console.log('导航到通知管理页面...');
|
||||
await this.page.goto('/notice');
|
||||
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await expect(this.page).toHaveURL(/.*notice/);
|
||||
|
||||
console.log('通知管理页面加载完成');
|
||||
} catch (error) {
|
||||
await this.page.screenshot({ path: `test-results/notification-error-${Date.now()}.png` });
|
||||
console.error('导航到通知管理页面失败:', error);
|
||||
throw new Error(`导航到通知管理页面失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async addNotification(title: string, content: string, type: string = '1', status: string = '0') {
|
||||
async addNotification(title: string, content: string) {
|
||||
await this.addButton.click();
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.titleInput.fill(title);
|
||||
await this.contentInput.fill(content);
|
||||
|
||||
|
||||
await this.saveButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async editNotification(title: string, newContent: string) {
|
||||
const row = this.table.locator('tr').filter({ hasText: title }).first();
|
||||
await row.locator('.el-button--primary').click();
|
||||
|
||||
const editBtn = row.getByRole('button', { name: '编辑' });
|
||||
await editBtn.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.contentInput.clear();
|
||||
await this.contentInput.fill(newContent);
|
||||
|
||||
|
||||
await this.saveButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async deleteNotification(title: string) {
|
||||
const row = this.table.locator('tr').filter({ hasText: title }).first();
|
||||
await row.locator('.el-button--danger').click();
|
||||
|
||||
await this.saveButton.click();
|
||||
const deleteBtn = row.getByRole('button', { name: '删除' });
|
||||
await deleteBtn.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmBtn = this.page.locator('.el-message-box').getByRole('button', { name: '确定' });
|
||||
await confirmBtn.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async searchNotification(keyword: string) {
|
||||
await this.searchInput.fill(keyword);
|
||||
await this.searchButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async clearSearch() {
|
||||
await this.searchInput.clear();
|
||||
await this.searchButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async verifyTableContains(text: string) {
|
||||
await expect(this.table).toContainText(text);
|
||||
}
|
||||
|
||||
async verifyTableNotContains(text: string) {
|
||||
await expect(this.table).not.toContainText(text);
|
||||
}
|
||||
|
||||
async getTableRowCount() {
|
||||
const rows = await this.table.locator('.el-table__row').count();
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
async verifyTableContains(text: string) {
|
||||
await expect(this.table).toContainText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,31 +4,23 @@ export class SystemConfigPage {
|
||||
readonly page: Page;
|
||||
readonly table;
|
||||
readonly addButton;
|
||||
readonly editButton;
|
||||
readonly deleteButton;
|
||||
readonly saveButton;
|
||||
readonly cancelButton;
|
||||
readonly searchInput;
|
||||
readonly searchButton;
|
||||
readonly dialog;
|
||||
readonly configNameInput;
|
||||
readonly configKeyInput;
|
||||
readonly configValueInput;
|
||||
readonly configTypeSelect;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.locator('.el-table');
|
||||
this.addButton = page.getByRole('button', { name: '新增配置' });
|
||||
this.editButton = page.getByRole('button', { name: '编辑' });
|
||||
this.deleteButton = page.getByRole('button', { name: '删除' });
|
||||
this.saveButton = page.getByRole('button', { name: '确定' });
|
||||
this.cancelButton = page.getByRole('button', { name: '取消' });
|
||||
this.searchInput = page.getByPlaceholder('搜索配置名称');
|
||||
this.searchButton = page.getByRole('button', { name: '搜索' });
|
||||
this.configNameInput = page.getByPlaceholder('请输入配置名称');
|
||||
this.configKeyInput = page.getByPlaceholder('请输入配置键名');
|
||||
this.configValueInput = page.getByPlaceholder('请输入配置键值');
|
||||
this.configTypeSelect = page.locator('.el-select');
|
||||
this.dialog = page.locator('.el-dialog');
|
||||
this.configNameInput = page.locator('.el-dialog').getByRole('textbox', { name: '参数名称' });
|
||||
this.configKeyInput = page.locator('.el-dialog').getByRole('textbox', { name: '参数键名' });
|
||||
this.configValueInput = page.locator('.el-dialog').getByRole('textbox', { name: '参数值' });
|
||||
}
|
||||
|
||||
async goto() {
|
||||
@@ -48,8 +40,9 @@ export class SystemConfigPage {
|
||||
}
|
||||
}
|
||||
|
||||
async addConfig(configName: string, configKey: string, configValue: string, configType: string = 'Y') {
|
||||
async addConfig(configName: string, configKey: string, configValue: string) {
|
||||
await this.addButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.configNameInput.fill(configName);
|
||||
await this.configKeyInput.fill(configKey);
|
||||
@@ -61,7 +54,9 @@ export class SystemConfigPage {
|
||||
|
||||
async editConfig(configKey: string, newValue: string) {
|
||||
const row = this.table.locator('tr').filter({ hasText: configKey }).first();
|
||||
await row.locator('.el-button--primary').click();
|
||||
const editBtn = row.getByRole('button', { name: '编辑' });
|
||||
await editBtn.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.configValueInput.clear();
|
||||
await this.configValueInput.fill(newValue);
|
||||
@@ -72,34 +67,21 @@ export class SystemConfigPage {
|
||||
|
||||
async deleteConfig(configKey: string) {
|
||||
const row = this.table.locator('tr').filter({ hasText: configKey }).first();
|
||||
await row.locator('.el-button--danger').click();
|
||||
const deleteBtn = row.getByRole('button', { name: '删除' });
|
||||
await deleteBtn.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.saveButton.click();
|
||||
const confirmBtn = this.page.locator('.el-message-box').getByRole('button', { name: '确定' });
|
||||
await confirmBtn.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async searchConfig(keyword: string) {
|
||||
await this.searchInput.fill(keyword);
|
||||
await this.searchButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async clearSearch() {
|
||||
await this.searchInput.clear();
|
||||
await this.searchButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async verifyTableContains(text: string) {
|
||||
await expect(this.table).toContainText(text);
|
||||
}
|
||||
|
||||
async verifyTableNotContains(text: string) {
|
||||
await expect(this.table).not.toContainText(text);
|
||||
}
|
||||
|
||||
async getTableRowCount() {
|
||||
const rows = await this.table.locator('.el-table__row').count();
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
async verifyTableContains(text: string) {
|
||||
await expect(this.table).toContainText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
import { RoleManagementPage } from './pages/RoleManagementPage';
|
||||
import { MenuManagementPage } from './pages/MenuManagementPage';
|
||||
import { SystemConfigPage } from './pages/SystemConfigPage';
|
||||
|
||||
// 测试用户配置
|
||||
const TEST_USERS = {
|
||||
superAdmin: {
|
||||
username: 'admin',
|
||||
password: 'password',
|
||||
role: '超级管理员'
|
||||
},
|
||||
systemAdmin: {
|
||||
username: 'sysadmin',
|
||||
password: 'SysAdmin123!',
|
||||
role: '系统管理员'
|
||||
},
|
||||
regularUser: {
|
||||
username: 'user',
|
||||
password: 'User123!',
|
||||
role: '普通用户'
|
||||
},
|
||||
guest: {
|
||||
username: '',
|
||||
password: '',
|
||||
role: '访客'
|
||||
}
|
||||
};
|
||||
|
||||
// 权限验证测试套件
|
||||
test.describe('系统配置功能权限验证测试', () => {
|
||||
let loginPage: LoginPage;
|
||||
let dashboardPage: DashboardPage;
|
||||
let userManagementPage: UserManagementPage;
|
||||
let roleManagementPage: RoleManagementPage;
|
||||
let menuManagementPage: MenuManagementPage;
|
||||
let systemConfigPage: SystemConfigPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
userManagementPage = new UserManagementPage(page);
|
||||
roleManagementPage = new RoleManagementPage(page);
|
||||
menuManagementPage = new MenuManagementPage(page);
|
||||
systemConfigPage = new SystemConfigPage(page);
|
||||
});
|
||||
|
||||
// 测试1: 超级管理员权限验证
|
||||
test('PERM-001: 超级管理员完整权限验证', async ({ page }) => {
|
||||
const user = TEST_USERS.superAdmin;
|
||||
const testResults = [];
|
||||
|
||||
await test.step(`1. ${user.role}登录系统`, async () => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login(user.username, user.password);
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
testResults.push({ step: '登录系统', result: '通过', details: '成功登录到仪表板' });
|
||||
});
|
||||
|
||||
await test.step('2. 验证用户管理权限', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
|
||||
// 验证用户管理页面可访问
|
||||
await expect(page.locator('.user-management-header')).toBeVisible();
|
||||
|
||||
// 验证创建用户权限
|
||||
await userManagementPage.clickCreateUser();
|
||||
await expect(page.locator('.user-form')).toBeVisible();
|
||||
testResults.push({ step: '用户管理权限', result: '通过', details: '可访问用户管理页面和创建用户功能' });
|
||||
});
|
||||
|
||||
await test.step('3. 验证角色管理权限', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
|
||||
// 验证角色管理页面可访问
|
||||
await expect(page.locator('.role-management-header')).toBeVisible();
|
||||
|
||||
// 验证创建角色权限
|
||||
await roleManagementPage.clickCreateRole();
|
||||
await expect(page.locator('.role-form')).toBeVisible();
|
||||
testResults.push({ step: '角色管理权限', result: '通过', details: '可访问角色管理页面和创建角色功能' });
|
||||
});
|
||||
|
||||
await test.step('4. 验证菜单管理权限', async () => {
|
||||
await dashboardPage.navigateToMenuManagement();
|
||||
|
||||
// 验证菜单管理页面可访问
|
||||
await expect(page.locator('.menu-management-header')).toBeVisible();
|
||||
|
||||
// 验证创建菜单权限
|
||||
await menuManagementPage.clickCreateMenu();
|
||||
await expect(page.locator('.menu-form')).toBeVisible();
|
||||
testResults.push({ step: '菜单管理权限', result: '通过', details: '可访问菜单管理页面和创建菜单功能' });
|
||||
});
|
||||
|
||||
await test.step('5. 验证系统配置权限', async () => {
|
||||
await dashboardPage.navigateToSystemConfig();
|
||||
|
||||
// 验证系统配置页面可访问
|
||||
await expect(page.locator('.system-config-header')).toBeVisible();
|
||||
|
||||
// 验证配置修改权限
|
||||
await systemConfigPage.clickEditConfig();
|
||||
await expect(page.locator('.config-form')).toBeVisible();
|
||||
testResults.push({ step: '系统配置权限', result: '通过', details: '可访问系统配置页面和修改配置功能' });
|
||||
});
|
||||
|
||||
// 生成测试报告
|
||||
console.log(`\n=== ${user.role}权限验证报告 ===`);
|
||||
testResults.forEach(result => {
|
||||
console.log(`[${result.result}] ${result.step}: ${result.details}`);
|
||||
});
|
||||
});
|
||||
|
||||
// 测试2: 系统管理员权限验证
|
||||
test('PERM-002: 系统管理员权限验证', async ({ page }) => {
|
||||
const user = TEST_USERS.systemAdmin;
|
||||
const testResults = [];
|
||||
|
||||
await test.step(`1. ${user.role}登录系统`, async () => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login(user.username, user.password);
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
testResults.push({ step: '登录系统', result: '通过', details: '成功登录到仪表板' });
|
||||
});
|
||||
|
||||
await test.step('2. 验证用户管理权限', async () => {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
|
||||
// 验证用户管理页面可访问
|
||||
await expect(page.locator('.user-management-header')).toBeVisible();
|
||||
|
||||
// 验证创建用户权限
|
||||
await userManagementPage.clickCreateUser();
|
||||
await expect(page.locator('.user-form')).toBeVisible();
|
||||
testResults.push({ step: '用户管理权限', result: '通过', details: '可访问用户管理页面和创建用户功能' });
|
||||
});
|
||||
|
||||
await test.step('3. 验证角色管理权限', async () => {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
|
||||
// 验证角色管理页面可访问
|
||||
await expect(page.locator('.role-management-header')).toBeVisible();
|
||||
|
||||
// 验证创建角色权限
|
||||
await roleManagementPage.clickCreateRole();
|
||||
await expect(page.locator('.role-form')).toBeVisible();
|
||||
testResults.push({ step: '角色管理权限', result: '通过', details: '可访问角色管理页面和创建角色功能' });
|
||||
});
|
||||
|
||||
await test.step('4. 验证菜单管理权限', async () => {
|
||||
await dashboardPage.navigateToMenuManagement();
|
||||
|
||||
// 验证菜单管理页面可访问
|
||||
await expect(page.locator('.menu-management-header')).toBeVisible();
|
||||
|
||||
// 验证创建菜单权限
|
||||
await menuManagementPage.clickCreateMenu();
|
||||
await expect(page.locator('.menu-form')).toBeVisible();
|
||||
testResults.push({ step: '菜单管理权限', result: '通过', details: '可访问菜单管理页面和创建菜单功能' });
|
||||
});
|
||||
|
||||
await test.step('5. 验证系统配置权限限制', async () => {
|
||||
await dashboardPage.navigateToSystemConfig();
|
||||
|
||||
// 验证系统配置页面可访问
|
||||
await expect(page.locator('.system-config-header')).toBeVisible();
|
||||
|
||||
// 验证配置修改权限(可能受限)
|
||||
try {
|
||||
await systemConfigPage.clickEditConfig();
|
||||
await expect(page.locator('.config-form')).toBeVisible();
|
||||
testResults.push({ step: '系统配置权限', result: '通过', details: '可访问系统配置页面和修改配置功能' });
|
||||
} catch (error) {
|
||||
testResults.push({ step: '系统配置权限', result: '受限', details: '系统配置修改功能受限' });
|
||||
}
|
||||
});
|
||||
|
||||
// 生成测试报告
|
||||
console.log(`\n=== ${user.role}权限验证报告 ===`);
|
||||
testResults.forEach(result => {
|
||||
console.log(`[${result.result}] ${step}: ${result.details}`);
|
||||
});
|
||||
});
|
||||
|
||||
// 测试3: 普通用户权限验证
|
||||
test('PERM-003: 普通用户权限验证', async ({ page }) => {
|
||||
const user = TEST_USERS.regularUser;
|
||||
const testResults = [];
|
||||
|
||||
await test.step(`1. ${user.role}登录系统`, async () => {
|
||||
await loginPage.goto();
|
||||
await loginPage.login(user.username, user.password);
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
testResults.push({ step: '登录系统', result: '通过', details: '成功登录到仪表板' });
|
||||
});
|
||||
|
||||
await test.step('2. 验证用户管理权限限制', async () => {
|
||||
try {
|
||||
await dashboardPage.navigateToUserManagement();
|
||||
|
||||
// 如果能够访问,验证是否有限制
|
||||
const hasAccess = await page.locator('.user-management-header').isVisible();
|
||||
if (hasAccess) {
|
||||
testResults.push({ step: '用户管理权限', result: '受限', details: '可访问但功能受限' });
|
||||
} else {
|
||||
testResults.push({ step: '用户管理权限', result: '拒绝', details: '无法访问用户管理页面' });
|
||||
}
|
||||
} catch (error) {
|
||||
testResults.push({ step: '用户管理权限', result: '拒绝', details: '权限不足,无法访问' });
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('3. 验证角色管理权限限制', async () => {
|
||||
try {
|
||||
await dashboardPage.navigateToRoleManagement();
|
||||
|
||||
const hasAccess = await page.locator('.role-management-header').isVisible();
|
||||
if (hasAccess) {
|
||||
testResults.push({ step: '角色管理权限', result: '受限', details: '可访问但功能受限' });
|
||||
} else {
|
||||
testResults.push({ step: '角色管理权限', result: '拒绝', details: '无法访问角色管理页面' });
|
||||
}
|
||||
} catch (error) {
|
||||
testResults.push({ step: '角色管理权限', result: '拒绝', details: '权限不足,无法访问' });
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('4. 验证菜单管理权限限制', async () => {
|
||||
try {
|
||||
await dashboardPage.navigateToMenuManagement();
|
||||
|
||||
const hasAccess = await page.locator('.menu-management-header').isVisible();
|
||||
if (hasAccess) {
|
||||
testResults.push({ step: '菜单管理权限', result: '受限', details: '可访问但功能受限' });
|
||||
} else {
|
||||
testResults.push({ step: '菜单管理权限', result: '拒绝', details: '无法访问菜单管理页面' });
|
||||
}
|
||||
} catch (error) {
|
||||
testResults.push({ step: '菜单管理权限', result: '拒绝', details: '权限不足,无法访问' });
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('5. 验证系统配置权限限制', async () => {
|
||||
try {
|
||||
await dashboardPage.navigateToSystemConfig();
|
||||
|
||||
const hasAccess = await page.locator('.system-config-header').isVisible();
|
||||
if (hasAccess) {
|
||||
testResults.push({ step: '系统配置权限', result: '受限', details: '可访问但功能受限' });
|
||||
} else {
|
||||
testResults.push({ step: '系统配置权限', result: '拒绝', details: '无法访问系统配置页面' });
|
||||
}
|
||||
} catch (error) {
|
||||
testResults.push({ step: '系统配置权限', result: '拒绝', details: '权限不足,无法访问' });
|
||||
}
|
||||
});
|
||||
|
||||
// 生成测试报告
|
||||
console.log(`\n=== ${user.role}权限验证报告 ===`);
|
||||
testResults.forEach(result => {
|
||||
console.log(`[${result.result}] ${result.step}: ${result.details}`);
|
||||
});
|
||||
});
|
||||
|
||||
// 测试4: 访客权限验证
|
||||
test('PERM-004: 访客权限验证', async ({ page }) => {
|
||||
const user = TEST_USERS.guest;
|
||||
const testResults = [];
|
||||
|
||||
await test.step('1. 直接访问系统管理页面', async () => {
|
||||
await page.goto('/user-management');
|
||||
|
||||
// 验证是否被重定向到登录页面
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes('/login')) {
|
||||
testResults.push({ step: '用户管理页面访问', result: '拒绝', details: '被重定向到登录页面' });
|
||||
} else {
|
||||
testResults.push({ step: '用户管理页面访问', result: '异常', details: '未正确重定向' });
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('2. 直接访问角色管理页面', async () => {
|
||||
await page.goto('/role-management');
|
||||
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes('/login')) {
|
||||
testResults.push({ step: '角色管理页面访问', result: '拒绝', details: '被重定向到登录页面' });
|
||||
} else {
|
||||
testResults.push({ step: '角色管理页面访问', result: '异常', details: '未正确重定向' });
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('3. 直接访问菜单管理页面', async () => {
|
||||
await page.goto('/menu-management');
|
||||
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes('/login')) {
|
||||
testResults.push({ step: '菜单管理页面访问', result: '拒绝', details: '被重定向到登录页面' });
|
||||
} else {
|
||||
testResults.push({ step: '菜单管理页面访问', result: '异常', details: '未正确重定向' });
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('4. 直接访问系统配置页面', async () => {
|
||||
await page.goto('/system-config');
|
||||
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes('/login')) {
|
||||
testResults.push({ step: '系统配置页面访问', result: '拒绝', details: '被重定向到登录页面' });
|
||||
} else {
|
||||
testResults.push({ step: '系统配置页面访问', result: '异常', details: '未正确重定向' });
|
||||
}
|
||||
});
|
||||
|
||||
// 生成测试报告
|
||||
console.log(`\n=== ${user.role}权限验证报告 ===`);
|
||||
testResults.forEach(result => {
|
||||
console.log(`[${result.result}] ${result.step}: ${result.details}`);
|
||||
});
|
||||
});
|
||||
|
||||
// 测试5: 权限边界测试
|
||||
test('PERM-005: 权限边界测试', async ({ page }) => {
|
||||
const testResults = [];
|
||||
|
||||
await test.step('1. 测试越权访问', async () => {
|
||||
// 使用普通用户登录
|
||||
await loginPage.goto();
|
||||
await loginPage.login(TEST_USERS.regularUser.username, TEST_USERS.regularUser.password);
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
|
||||
// 尝试直接访问管理员功能URL
|
||||
await page.goto('/user-management/create');
|
||||
|
||||
// 验证是否被阻止
|
||||
const isBlocked = await page.locator('.access-denied, .permission-error').isVisible() ||
|
||||
page.url().includes('/login') ||
|
||||
page.url().includes('/dashboard');
|
||||
|
||||
if (isBlocked) {
|
||||
testResults.push({ step: '越权访问测试', result: '通过', details: '系统正确阻止了越权访问' });
|
||||
} else {
|
||||
testResults.push({ step: '越权访问测试', result: '失败', details: '系统未正确阻止越权访问' });
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('2. 测试API权限验证', async () => {
|
||||
// 模拟API调用权限验证
|
||||
const apiResponse = await page.request.get('/api/users');
|
||||
|
||||
if (apiResponse.status() === 401 || apiResponse.status() === 403) {
|
||||
testResults.push({ step: 'API权限验证', result: '通过', details: 'API权限验证正常工作' });
|
||||
} else {
|
||||
testResults.push({ step: 'API权限验证', result: '警告', details: 'API权限验证可能需要加强' });
|
||||
}
|
||||
});
|
||||
|
||||
// 生成测试报告
|
||||
console.log('\n=== 权限边界测试报告 ===');
|
||||
testResults.forEach(result => {
|
||||
console.log(`[${result.result}] ${result.step}: ${result.details}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,256 +0,0 @@
|
||||
# 基于角色的用户模拟测试套件
|
||||
|
||||
## 概述
|
||||
|
||||
本测试套件实现了基于角色的用户模拟测试,用于验证后端管理系统的权限边界和业务流程。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 核心组件
|
||||
|
||||
1. **角色定义系统** (`roles/`)
|
||||
- `base.role.ts` - 角色定义基类
|
||||
- `admin.role.ts` - 管理员角色
|
||||
- `user.role.ts` - 普通用户角色
|
||||
- `test.role.ts` - 测试用户角色
|
||||
- `role-factory.ts` - 角色工厂
|
||||
|
||||
2. **共享工具** (`shared/`)
|
||||
- `role-auth-manager.ts` - Token管理器
|
||||
- `auth-helper.ts` - 认证辅助工具
|
||||
- `test-data-manager.ts` - 测试数据管理器
|
||||
- `permission-helper.ts` - 权限验证工具
|
||||
|
||||
3. **测试场景** (`scenarios/`)
|
||||
- `authentication/` - 认证场景测试
|
||||
- `user-management/` - 用户管理场景测试
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境准备
|
||||
|
||||
1. 确保后端服务运行在 `http://localhost:8084`
|
||||
2. 确保前端服务运行在 `http://localhost:3002`
|
||||
3. 确保H2数据库已初始化测试数据
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有单元测试
|
||||
pnpm test
|
||||
|
||||
# 运行角色测试项目
|
||||
pnpm exec playwright test --project=role-based-tests
|
||||
|
||||
# 运行特定测试文件
|
||||
pnpm exec playwright test e2e/role-based-tests/scenarios/authentication/login-flow.spec.ts
|
||||
|
||||
# 运行特定角色的测试
|
||||
pnpm exec playwright test --project=role-based-tests --grep "管理员"
|
||||
```
|
||||
|
||||
## 角色配置
|
||||
|
||||
### 测试用户
|
||||
|
||||
所有测试用户统一使用密码:`Test@123`
|
||||
|
||||
| 用户名 | 角色 | 说明 |
|
||||
|--------|------|------|
|
||||
| admin | 超级管理员 | 拥有所有权限 |
|
||||
| normaluser | 普通用户 | 只能访问个人信息 |
|
||||
| e2e_test_user | 测试用户 | 用于E2E测试 |
|
||||
|
||||
### 权限定义
|
||||
|
||||
每个角色定义包含:
|
||||
- `permissions` - 拥有的权限列表
|
||||
- `cannotAccess` - 无法访问的路径
|
||||
- `expectedBehaviors` - 预期行为(CRUD权限)
|
||||
|
||||
## 测试场景
|
||||
|
||||
### 认证场景
|
||||
|
||||
- 登录流程测试(6个测试用例)
|
||||
- 管理员用户登录成功
|
||||
- 普通用户登录成功
|
||||
- 错误密码登录失败
|
||||
- 空用户名登录失败
|
||||
- 空密码登录失败
|
||||
- Token注入登录
|
||||
|
||||
- 登出流程测试(4个测试用例)
|
||||
- 用户登出成功
|
||||
- 登出后无法访问受保护页面
|
||||
- 登出后Token被清除
|
||||
- 多角色登出测试
|
||||
|
||||
### 用户管理场景
|
||||
|
||||
- 管理员创建用户测试(5个测试用例)
|
||||
- 管理员可以创建新用户
|
||||
- 管理员可以编辑用户信息
|
||||
- 管理员可以删除用户
|
||||
- 创建用户时用户名重复验证
|
||||
- 创建用户时邮箱格式验证
|
||||
|
||||
- 权限边界验证测试(11个测试用例)
|
||||
- 管理员权限验证(5个)
|
||||
- 普通用户权限验证(4个)
|
||||
- 测试用户权限验证(2个)
|
||||
- 跨角色权限对比测试
|
||||
|
||||
## 测试数据管理
|
||||
|
||||
### 自动清理
|
||||
|
||||
测试数据管理器会自动跟踪创建的测试数据,并在测试结束后清理:
|
||||
|
||||
```typescript
|
||||
import { getTestDataManager } from '../shared/test-data-manager';
|
||||
|
||||
test.afterEach(async () => {
|
||||
await getTestDataManager().cleanup('user');
|
||||
});
|
||||
```
|
||||
|
||||
### 手动创建测试数据
|
||||
|
||||
```typescript
|
||||
const testDataManager = getTestDataManager();
|
||||
|
||||
const user = await testDataManager.createUser({
|
||||
username: 'testuser',
|
||||
password: 'Test@123',
|
||||
email: 'test@example.com',
|
||||
});
|
||||
```
|
||||
|
||||
## 认证方式
|
||||
|
||||
### Token注入(推荐)
|
||||
|
||||
```typescript
|
||||
import { createAuthenticatedPage } from '../shared/auth-helper';
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await createAuthenticatedPage(page, context, 'admin');
|
||||
});
|
||||
```
|
||||
|
||||
### 真实登录
|
||||
|
||||
```typescript
|
||||
import { AuthHelper } from '../shared/auth-helper';
|
||||
|
||||
const authHelper = new AuthHelper(page, context);
|
||||
await authHelper.loginAsRole('admin', false); // false表示使用真实登录
|
||||
```
|
||||
|
||||
## 权限验证
|
||||
|
||||
```typescript
|
||||
import { createPermissionHelper } from '../shared/permission-helper';
|
||||
|
||||
const permissionHelper = createPermissionHelper(page);
|
||||
|
||||
// 验证可以访问
|
||||
await permissionHelper.verifyCanAccess('/user-management');
|
||||
|
||||
// 验证无法访问
|
||||
await permissionHelper.verifyCannotAccess('/role-management');
|
||||
|
||||
// 验证角色权限边界
|
||||
const role = RoleFactory.getRole('admin');
|
||||
await permissionHelper.verifyRolePermissions(role);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用Token注入**:提升测试执行效率
|
||||
2. **遵循TDD原则**:先写测试,再实现功能
|
||||
3. **测试数据隔离**:每个测试独立创建和清理数据
|
||||
4. **权限边界验证**:确保每个角色的权限边界清晰
|
||||
5. **跨浏览器测试**:在Chrome、Firefox、Safari上运行测试
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 登录失败
|
||||
|
||||
1. 检查后端服务是否运行
|
||||
2. 检查数据库是否初始化
|
||||
3. 检查密码是否正确(应为 `Test@123`)
|
||||
|
||||
### 权限验证失败
|
||||
|
||||
1. 检查角色定义是否正确
|
||||
2. 检查后端权限配置
|
||||
3. 检查前端路由守卫
|
||||
|
||||
### 测试数据清理失败
|
||||
|
||||
1. 检查数据库连接
|
||||
2. 检查API权限
|
||||
3. 手动清理测试数据
|
||||
|
||||
## CI/CD集成
|
||||
|
||||
### Jenkins Pipeline示例
|
||||
|
||||
```groovy
|
||||
stage('Role-Based Tests') {
|
||||
steps {
|
||||
sh 'pnpm install'
|
||||
sh 'pnpm exec playwright test --project=role-based-tests'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
publishHTML([
|
||||
allowMissing: false,
|
||||
alwaysLinkToLastBuild: true,
|
||||
keepAll: true,
|
||||
reportDir: 'playwright-report',
|
||||
reportFiles: 'index.html',
|
||||
reportName: 'Playwright Report'
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 维护指南
|
||||
|
||||
### 添加新角色
|
||||
|
||||
1. 在 `roles/` 目录创建新的角色定义文件
|
||||
2. 在 `role-factory.ts` 中注册新角色
|
||||
3. 在 `data-h2.sql` 中添加测试用户数据
|
||||
4. 编写对应的测试用例
|
||||
|
||||
### 添加新测试场景
|
||||
|
||||
1. 在 `scenarios/` 目录创建新的测试文件
|
||||
2. 使用现有的工具类(认证、数据管理、权限验证)
|
||||
3. 确保测试数据隔离和清理
|
||||
4. 更新文档
|
||||
|
||||
## 统计信息
|
||||
|
||||
- **单元测试**:172个测试用例
|
||||
- **E2E测试**:26个测试场景
|
||||
- **角色定义**:3个角色
|
||||
- **测试覆盖率**:核心功能100%
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2026-04-04)
|
||||
|
||||
- ✅ 实现角色定义系统
|
||||
- ✅ 实现认证辅助工具
|
||||
- ✅ 实现测试数据管理器
|
||||
- ✅ 实现权限验证工具
|
||||
- ✅ 实现认证场景测试
|
||||
- ✅ 实现用户管理场景测试
|
||||
- ✅ 统一H2数据库密码配置
|
||||
- ✅ 配置Playwright测试项目
|
||||
@@ -1,83 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { RoleFactory } from '@/role-based-tests/roles/role-factory';
|
||||
import { createAuthenticatedPage } from '@/role-based-tests/shared/auth-helper';
|
||||
|
||||
test.describe('登录流程测试', () => {
|
||||
test('管理员用户登录成功', async ({ page, context }) => {
|
||||
const role = RoleFactory.getRole('admin');
|
||||
|
||||
await page.goto('/login');
|
||||
|
||||
await page.fill('input[placeholder*="用户名"]', role.credentials.username);
|
||||
await page.fill('input[placeholder*="密码"]', role.credentials.password);
|
||||
await page.click('button:has-text("登录")');
|
||||
|
||||
await expect(page).toHaveURL(/\/(dashboard|\/)?/, { timeout: 10000 });
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('普通用户登录成功', async ({ page, context }) => {
|
||||
const role = RoleFactory.getRole('user');
|
||||
|
||||
await page.goto('/login');
|
||||
|
||||
await page.fill('input[placeholder*="用户名"]', role.credentials.username);
|
||||
await page.fill('input[placeholder*="密码"]', role.credentials.password);
|
||||
await page.click('button:has-text("登录")');
|
||||
|
||||
await expect(page).toHaveURL(/\/(dashboard|\/)?/, { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('错误密码登录失败', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
await page.fill('input[placeholder*="用户名"]', 'admin');
|
||||
await page.fill('input[placeholder*="密码"]', 'wrongpassword');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForResponse(resp => resp.url().includes('/auth/login') && resp.status() === 401),
|
||||
page.click('button:has-text("登录")')
|
||||
]);
|
||||
|
||||
const errorMessage = page.locator('.el-message');
|
||||
await expect(errorMessage).toBeVisible({ timeout: 10000 });
|
||||
await expect(errorMessage).toContainText(/用户名或密码错误|登录失败/i);
|
||||
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
|
||||
test('空用户名登录失败', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
await page.fill('input[placeholder*="密码"]', 'Test@123');
|
||||
await page.click('input[placeholder*="用户名"]');
|
||||
await page.click('input[placeholder*="密码"]');
|
||||
await page.click('button:has-text("登录")');
|
||||
|
||||
const validationMessage = page.locator('.el-form-item__error');
|
||||
await expect(validationMessage).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('空密码登录失败', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
await page.fill('input[placeholder*="用户名"]', 'admin');
|
||||
await page.click('input[placeholder*="密码"]');
|
||||
await page.click('input[placeholder*="用户名"]');
|
||||
await page.click('button:has-text("登录")');
|
||||
|
||||
const validationMessage = page.locator('.el-form-item__error');
|
||||
await expect(validationMessage).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('Token注入登录', async ({ page, context }) => {
|
||||
await createAuthenticatedPage(page, context, 'admin');
|
||||
|
||||
await page.goto('/dashboard');
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { RoleFactory } from '@/role-based-tests/roles/role-factory';
|
||||
import { AuthHelper } from '@/role-based-tests/shared/auth-helper';
|
||||
|
||||
test.describe('登出流程测试', () => {
|
||||
let authHelper: AuthHelper;
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
authHelper = new AuthHelper(page, context);
|
||||
await authHelper.loginAsRole('admin');
|
||||
});
|
||||
|
||||
test('用户登出成功', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
|
||||
await page.waitForSelector('.el-dropdown', { state: 'visible' });
|
||||
await page.click('.el-dropdown .el-avatar');
|
||||
await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 });
|
||||
await page.click('.el-dropdown-menu-item:has-text("退出登录")');
|
||||
|
||||
await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
|
||||
|
||||
const loginButton = page.locator('button:has-text("登录")');
|
||||
await expect(loginButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('登出后无法访问受保护页面', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
|
||||
await page.waitForSelector('.el-dropdown', { state: 'visible' });
|
||||
await page.click('.el-dropdown .el-avatar');
|
||||
await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 });
|
||||
await page.click('.el-dropdown-menu-item:has-text("退出登录")');
|
||||
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
await page.goto('/users');
|
||||
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
|
||||
test('登出后Token被清除', async ({ page, context }) => {
|
||||
await page.goto('/dashboard');
|
||||
|
||||
await page.waitForSelector('.el-dropdown', { state: 'visible' });
|
||||
await page.click('.el-dropdown .el-avatar');
|
||||
await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 });
|
||||
await page.click('.el-dropdown-menu-item:has-text("退出登录")');
|
||||
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
const cookies = await context.cookies();
|
||||
const tokenCookie = cookies.find(c => c.name === 'token');
|
||||
expect(tokenCookie).toBeUndefined();
|
||||
|
||||
const localStorageToken = await page.evaluate(() => {
|
||||
return localStorage.getItem('token');
|
||||
});
|
||||
expect(localStorageToken).toBeNull();
|
||||
});
|
||||
|
||||
test('多角色登出测试', async ({ page, context }) => {
|
||||
const roles = ['admin', 'user', 'test'];
|
||||
|
||||
for (const roleName of roles) {
|
||||
const helper = new AuthHelper(page, context);
|
||||
await helper.clearAuth();
|
||||
await helper.loginAsRole(roleName);
|
||||
|
||||
await page.goto('/dashboard');
|
||||
|
||||
await page.waitForSelector('.el-dropdown', { state: 'visible' });
|
||||
await page.click('.el-dropdown .el-avatar');
|
||||
await page.waitForSelector('.el-dropdown-menu', { state: 'visible', timeout: 3000 });
|
||||
await page.click('.el-dropdown-menu-item:has-text("退出登录")');
|
||||
|
||||
await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
|
||||
}
|
||||
});
|
||||
});
|
||||
-102
@@ -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 });
|
||||
});
|
||||
});
|
||||
-132
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user