refactor(tests): 迁移 E2E 测试到独立的 e2e-tests 目录
This commit is contained in:
@@ -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. 优先使用单元测试覆盖功能细节
|
||||
@@ -0,0 +1,65 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('API连通性测试', () => {
|
||||
test('验证网关服务健康状态', async ({ page }) => {
|
||||
await test.step('检查网关健康状态', async () => {
|
||||
const response = await page.request.get('http://localhost:8080/actuator/health');
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.status).toBe('UP');
|
||||
});
|
||||
|
||||
await test.step('检查应用服务路由', async () => {
|
||||
const response = await page.request.get('http://localhost:8080/api/auth/health');
|
||||
expect(response.status()).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
test('验证前端与后端连通性', async ({ page }) => {
|
||||
await test.step('加载前端应用', async () => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 验证页面标题
|
||||
const title = await page.title();
|
||||
expect(title).toContain('Novalon');
|
||||
});
|
||||
|
||||
await test.step('检查API请求', async () => {
|
||||
// 监听网络请求
|
||||
const apiRequests = [];
|
||||
page.on('request', request => {
|
||||
if (request.url().includes('/api/')) {
|
||||
apiRequests.push({
|
||||
url: request.url(),
|
||||
method: request.method()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 触发一些前端操作来生成API请求
|
||||
await page.goto('/login');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 验证是否有API请求发出
|
||||
expect(apiRequests.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('验证数据库连接状态', async ({ page }) => {
|
||||
await test.step('检查数据库健康状态', async () => {
|
||||
// 通过应用服务检查数据库连接
|
||||
const response = await page.request.get('http://localhost:8084/actuator/health');
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.status).toBe('UP');
|
||||
|
||||
// 检查数据库组件状态
|
||||
if (data.components && data.components.db) {
|
||||
expect(data.components.db.status).toBe('UP');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('认证和授权测试', () => {
|
||||
let authToken: string;
|
||||
let userId: number;
|
||||
|
||||
test('用户登录测试', async ({ page }) => {
|
||||
await test.step('准备登录数据', async () => {
|
||||
console.log('准备登录测试数据...');
|
||||
});
|
||||
|
||||
await test.step('发送登录请求', async () => {
|
||||
const response = await page.request.post('http://localhost:8080/api/auth/login', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty('token');
|
||||
expect(data).toHaveProperty('userId');
|
||||
expect(data).toHaveProperty('username');
|
||||
|
||||
authToken = data.token;
|
||||
userId = data.userId;
|
||||
|
||||
console.log('登录成功,获取到Token:', authToken.substring(0, 20) + '...');
|
||||
});
|
||||
|
||||
await test.step('验证Token有效性', async () => {
|
||||
const response = await page.request.get('http://localhost:8080/api/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
console.log('Token验证成功,可以访问受保护的资源');
|
||||
});
|
||||
});
|
||||
|
||||
test('用户信息查询测试', async ({ page }) => {
|
||||
await test.step('先登录获取Token', async () => {
|
||||
const loginResponse = await page.request.post('http://localhost:8080/api/auth/login', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
}
|
||||
});
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
authToken = loginData.token;
|
||||
userId = loginData.userId;
|
||||
});
|
||||
|
||||
await test.step('查询用户列表', async () => {
|
||||
const response = await page.request.get('http://localhost:8080/api/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const users = await response.json();
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
expect(users.length).toBeGreaterThan(0);
|
||||
|
||||
console.log(`查询到 ${users.length} 个用户`);
|
||||
});
|
||||
|
||||
await test.step('查询指定用户信息', async () => {
|
||||
const response = await page.request.get(`http://localhost:8080/api/users/${userId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const user = await response.json();
|
||||
expect(user).toHaveProperty('id');
|
||||
expect(user).toHaveProperty('username');
|
||||
expect(user.id).toBe(userId);
|
||||
|
||||
console.log(`查询到用户信息: ${user.username}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('权限验证测试', async ({ page }) => {
|
||||
await test.step('先登录获取Token', async () => {
|
||||
const loginResponse = await page.request.post('http://localhost:8080/api/auth/login', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
}
|
||||
});
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
authToken = loginData.token;
|
||||
});
|
||||
|
||||
await test.step('测试访问受保护的API', async () => {
|
||||
const protectedEndpoints = [
|
||||
'/api/users',
|
||||
'/api/roles',
|
||||
'/api/menus',
|
||||
'/api/config'
|
||||
];
|
||||
|
||||
for (const endpoint of protectedEndpoints) {
|
||||
const response = await page.request.get(`http://localhost:8080${endpoint}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`访问 ${endpoint}: ${response.status()}`);
|
||||
expect([200, 404]).toContain(response.status());
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('测试无Token访问受保护API', async () => {
|
||||
const response = await page.request.get('http://localhost:8080/api/users');
|
||||
|
||||
expect(response.status()).toBe(401);
|
||||
console.log('无Token访问受保护API返回401,权限验证正常');
|
||||
});
|
||||
});
|
||||
|
||||
test('前端登录流程测试', async ({ page }) => {
|
||||
await test.step('访问登录页面', async () => {
|
||||
await page.goto('/login');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 验证登录页面元素
|
||||
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]');
|
||||
const passwordInput = page.locator('input[type="password"]');
|
||||
const loginButton = page.locator('button:has-text("登录")');
|
||||
|
||||
expect(await usernameInput.count()).toBeGreaterThan(0);
|
||||
expect(await passwordInput.count()).toBeGreaterThan(0);
|
||||
expect(await loginButton.count()).toBeGreaterThan(0);
|
||||
|
||||
console.log('登录页面元素验证通过');
|
||||
});
|
||||
|
||||
await test.step('填写登录表单', async () => {
|
||||
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
|
||||
console.log('登录表单填写完成');
|
||||
});
|
||||
|
||||
await test.step('提交登录表单', async () => {
|
||||
const loginButton = page.locator('button:has-text("登录")').first();
|
||||
|
||||
// 监听响应
|
||||
const responsePromise = page.waitForResponse(response =>
|
||||
response.url().includes('/api/auth/login') && response.request().method() === 'POST'
|
||||
);
|
||||
|
||||
await loginButton.click();
|
||||
|
||||
try {
|
||||
const response = await responsePromise;
|
||||
console.log('登录请求状态:', response.status());
|
||||
|
||||
if (response.status() === 200) {
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty('token');
|
||||
console.log('前端登录成功');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('登录请求可能超时,但这是预期的行为');
|
||||
}
|
||||
|
||||
// 等待一段时间,观察页面变化
|
||||
await page.waitForTimeout(2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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('admin123');
|
||||
await page.locator('button:has-text("登录")').click();
|
||||
|
||||
await page.waitForURL('**/dashboard', { timeout: 30000 });
|
||||
|
||||
await page.context().storageState({ path: authFile });
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('基础UI功能测试', () => {
|
||||
test('前端应用基本功能验证', async ({ page }) => {
|
||||
// 测试1: 应用首页加载
|
||||
await test.step('加载应用首页', async () => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 验证页面标题
|
||||
const title = await page.title();
|
||||
expect(title).toContain('Novalon');
|
||||
});
|
||||
|
||||
// 测试2: 登录页面渲染
|
||||
await test.step('验证登录页面元素', async () => {
|
||||
await page.goto('/login');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 验证登录表单元素
|
||||
await expect(page.locator('input[type="text"]')).toBeVisible();
|
||||
await expect(page.locator('input[type="password"]')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("登录")')).toBeVisible();
|
||||
});
|
||||
|
||||
// 测试3: 页面导航
|
||||
await test.step('验证页面导航功能', async () => {
|
||||
// 检查页面是否有基本的导航元素 - 使用更灵活的选择器
|
||||
const navigationSelectors = [
|
||||
'nav', '.navbar', '.menu', '.el-menu', '.el-header',
|
||||
'.layout-header', '.app-header', '[class*="header"]',
|
||||
'[class*="nav"]', '[class*="menu"]'
|
||||
];
|
||||
|
||||
let hasNavigation = false;
|
||||
for (const selector of navigationSelectors) {
|
||||
const count = await page.locator(selector).count();
|
||||
if (count > 0) {
|
||||
hasNavigation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找不到传统导航元素,检查是否有其他页面结构
|
||||
if (!hasNavigation) {
|
||||
const hasAppContainer = await page.locator('#app, .app, .container').count() > 0;
|
||||
const hasBodyContent = await page.locator('body').textContent() !== '';
|
||||
hasNavigation = hasAppContainer && hasBodyContent;
|
||||
}
|
||||
|
||||
expect(hasNavigation).toBeTruthy();
|
||||
});
|
||||
|
||||
// 测试4: 响应式设计验证
|
||||
await test.step('验证响应式设计', async () => {
|
||||
// 设置移动端视口
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 验证页面在移动端仍然可访问
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('应用静态资源加载', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// 验证CSS加载
|
||||
const cssLoaded = await page.evaluate(() => {
|
||||
return document.styleSheets.length > 0;
|
||||
});
|
||||
expect(cssLoaded).toBeTruthy();
|
||||
|
||||
// 验证JavaScript加载
|
||||
const jsLoaded = await page.evaluate(() => {
|
||||
return typeof window !== 'undefined';
|
||||
});
|
||||
expect(jsLoaded).toBeTruthy();
|
||||
|
||||
// 验证Vue应用挂载
|
||||
const vueMounted = await page.evaluate(() => {
|
||||
return !!document.querySelector('#app');
|
||||
});
|
||||
expect(vueMounted).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('参数配置功能测试', () => {
|
||||
let authToken: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const response = await request.post('http://localhost:8080/api/auth/login', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
const data = await response.json();
|
||||
authToken = data.token;
|
||||
});
|
||||
|
||||
test('参数配置列表显示测试', async ({ page }) => {
|
||||
await test.step('导航到参数配置页面', async () => {
|
||||
await page.goto('http://localhost:3002/login');
|
||||
|
||||
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
const loginButton = page.locator('button:has-text("登录")').first();
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 点击系统管理菜单
|
||||
const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first();
|
||||
if (await systemMenu.count() > 0) {
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// 点击参数配置
|
||||
const configManagement = page.locator('.el-menu-item:has-text("参数配置")').first();
|
||||
if (await configManagement.count() > 0) {
|
||||
await configManagement.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证参数配置列表显示', async () => {
|
||||
// 检查是否有参数配置列表或表格
|
||||
const tableSelectors = [
|
||||
'table',
|
||||
'.el-table',
|
||||
'[class*="table"]',
|
||||
'.config-list'
|
||||
];
|
||||
|
||||
let foundTable = false;
|
||||
for (const selector of tableSelectors) {
|
||||
const count = await page.locator(selector).count();
|
||||
if (count > 0) {
|
||||
foundTable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(foundTable).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,429 @@
|
||||
import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
class CustomReporter implements Reporter {
|
||||
private results: Map<string, TestCase[]> = new Map();
|
||||
private suiteResults: Map<string, Suite> = new Map();
|
||||
private startTime: number = Date.now();
|
||||
private testResults: TestResult[] = [];
|
||||
|
||||
onBegin(config: FullConfig) {
|
||||
console.log(`🚀 开始测试执行: ${config.projects.map(p => p.name).join(', ')}`);
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
onTestBegin(test: TestCase, result: TestResult) {
|
||||
console.log(`📝 开始测试: ${test.title}`);
|
||||
}
|
||||
|
||||
onTestEnd(test: TestCase, result: TestResult) {
|
||||
console.log(`✅ 测试完成: ${test.title} - ${result.status}`);
|
||||
this.testResults.push(result);
|
||||
}
|
||||
|
||||
onEnd(result: FullResult) {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - this.startTime;
|
||||
|
||||
console.log(`🎉 测试执行完成`);
|
||||
console.log(`⏱️ 总耗时: ${this.formatDuration(duration)}`);
|
||||
|
||||
const stats = this.calculateStats(result);
|
||||
this.generateConsoleReport(stats);
|
||||
this.generateHtmlReport(result, stats);
|
||||
this.generateJsonReport(result, stats);
|
||||
}
|
||||
|
||||
private calculateStats(result: FullResult): TestStats {
|
||||
const 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);
|
||||
const avgDuration = totalDuration / allTests.length;
|
||||
|
||||
const passRate = (passed.length / allTests.length) * 100;
|
||||
const failRate = (failed.length / allTests.length) * 100;
|
||||
const skipRate = (skipped.length / allTests.length) * 100;
|
||||
const flakyRate = (flaky.length / allTests.length) * 100;
|
||||
|
||||
return {
|
||||
total: allTests.length,
|
||||
passed: passed.length,
|
||||
failed: failed.length,
|
||||
skipped: skipped.length,
|
||||
flaky: flaky.length,
|
||||
passRate,
|
||||
failRate,
|
||||
skipRate,
|
||||
flakyRate,
|
||||
totalDuration,
|
||||
avgDuration,
|
||||
slowestTests: allTests
|
||||
.filter(t => t.duration > 0)
|
||||
.sort((a, b) => b.duration - a.duration)
|
||||
.slice(0, 10),
|
||||
failedTests: failed,
|
||||
};
|
||||
}
|
||||
|
||||
private generateConsoleReport(stats: TestStats) {
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('📊 测试统计报告');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('');
|
||||
console.log(`📈 总测试数: ${stats.total}`);
|
||||
console.log(`✅ 通过: ${stats.passed} (${stats.passRate.toFixed(2)}%)`);
|
||||
console.log(`❌ 失败: ${stats.failed} (${stats.failRate.toFixed(2)}%)`);
|
||||
console.log(`⏭️ 跳过: ${stats.skipped} (${stats.skipRate.toFixed(2)}%)`);
|
||||
console.log(`🔄 不稳定: ${stats.flaky} (${stats.flakyRate.toFixed(2)}%)`);
|
||||
console.log('');
|
||||
console.log(`⏱️ 总耗时: ${this.formatDuration(stats.totalDuration)}`);
|
||||
console.log(`⏱️ 平均耗时: ${this.formatDuration(stats.avgDuration)}`);
|
||||
console.log('');
|
||||
console.log('🐌 最慢的10个测试:');
|
||||
stats.slowestTests.forEach((test, index) => {
|
||||
console.log(` ${index + 1}. ${test.title} - ${this.formatDuration(test.duration || 0)}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
if (stats.failedTests.length > 0) {
|
||||
console.log('❌ 失败的测试:');
|
||||
stats.failedTests.forEach((test, index) => {
|
||||
console.log(` ${index + 1}. ${test.title || '未命名测试'}`);
|
||||
if (test.location?.file) {
|
||||
console.log(` 位置: ${test.location.file}:${test.location.line || 0}`);
|
||||
}
|
||||
if (test.error?.message) {
|
||||
console.log(` 错误: ${test.error.message}`);
|
||||
}
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
private generateHtmlReport(result: FullResult, stats: TestStats) {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>测试报告 - Novalon管理系统</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
padding: 30px;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.stat-card h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.stat-card .value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
.stat-card .label {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.stat-card.passed {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
}
|
||||
.stat-card.failed {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #f44336 100%);
|
||||
}
|
||||
.stat-card.flaky {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #f093fb 100%);
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.section h2 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.test-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.test-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 4px solid #ddd;
|
||||
background: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.test-item.passed {
|
||||
border-left-color: #38ef7d;
|
||||
background: #f0fff4;
|
||||
}
|
||||
.test-item.failed {
|
||||
border-left-color: #ef4444;
|
||||
background: #fff5f5;
|
||||
}
|
||||
.test-item.skipped {
|
||||
border-left-color: #f59e0b;
|
||||
background: #fef9c3;
|
||||
}
|
||||
.test-item.flaky {
|
||||
border-left-color: #f093fb;
|
||||
background: #fef3c7;
|
||||
}
|
||||
.test-item .test-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
.test-item .test-duration {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
.test-item .test-error {
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
padding: 10px;
|
||||
background: #fee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 20px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🧪 Novalon管理系统测试报告</h1>
|
||||
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card passed">
|
||||
<h3>通过测试</h3>
|
||||
<div class="value">${stats.passed}</div>
|
||||
<div class="label">${stats.passRate.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div class="stat-card failed">
|
||||
<h3>失败测试</h3>
|
||||
<div class="value">${stats.failed}</div>
|
||||
<div class="label">${stats.failRate.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div class="stat-card flaky">
|
||||
<h3>不稳定测试</h3>
|
||||
<div class="value">${stats.flaky}</div>
|
||||
<div class="label">${stats.flakyRate.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>总测试数</h3>
|
||||
<div class="value">${stats.total}</div>
|
||||
<div class="label">100%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-fill" style="width: ${stats.passRate}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📈 测试统计</h2>
|
||||
<ul class="test-list">
|
||||
<li class="test-item">
|
||||
<div class="test-name">总耗时</div>
|
||||
<div class="test-duration">${this.formatDuration(stats.totalDuration)}</div>
|
||||
</li>
|
||||
<li class="test-item">
|
||||
<div class="test-name">平均耗时</div>
|
||||
<div class="test-duration">${this.formatDuration(stats.avgDuration)}</div>
|
||||
</li>
|
||||
<li class="test-item">
|
||||
<div class="test-name">跳过测试</div>
|
||||
<div class="test-duration">${stats.skipped} (${stats.skipRate.toFixed(2)}%)</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
${stats.failedTests.length > 0 ? `
|
||||
<div class="section">
|
||||
<h2>❌ 失败测试详情</h2>
|
||||
<ul class="test-list">
|
||||
${stats.failedTests.map(test => `
|
||||
<li class="test-item failed">
|
||||
<div class="test-name">${test.title}</div>
|
||||
<div class="test-duration">${this.formatDuration(test.duration || 0)}</div>
|
||||
<div class="test-error">
|
||||
<strong>错误:</strong> ${test.error?.message || '未知错误'}
|
||||
</div>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="section">
|
||||
<h2>🐌 最慢的10个测试</h2>
|
||||
<ul class="test-list">
|
||||
${stats.slowestTests.map((test, index) => `
|
||||
<li class="test-item ${test.status}">
|
||||
<div class="test-name">${index + 1}. ${test.title}</div>
|
||||
<div class="test-duration">${this.formatDuration(test.duration || 0)}</div>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>🧪 Novalon管理系统 - 自动化测试报告</p>
|
||||
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.html');
|
||||
fs.writeFileSync(reportPath, html, 'utf-8');
|
||||
console.log(`📄 HTML报告已生成: ${reportPath}`);
|
||||
}
|
||||
|
||||
private generateJsonReport(result: FullResult, stats: TestStats) {
|
||||
const report = {
|
||||
summary: {
|
||||
timestamp: new Date().toISOString(),
|
||||
total: stats.total,
|
||||
passed: stats.passed,
|
||||
failed: stats.failed,
|
||||
skipped: stats.skipped,
|
||||
flaky: stats.flaky,
|
||||
passRate: stats.passRate,
|
||||
failRate: stats.failRate,
|
||||
skipRate: stats.skipRate,
|
||||
flakyRate: stats.flakyRate,
|
||||
totalDuration: stats.totalDuration,
|
||||
avgDuration: stats.avgDuration,
|
||||
},
|
||||
failedTests: stats.failedTests.map(test => ({
|
||||
title: test.title,
|
||||
location: test.location,
|
||||
error: test.error?.message,
|
||||
duration: test.duration,
|
||||
})),
|
||||
slowestTests: stats.slowestTests.map(test => ({
|
||||
title: test.title,
|
||||
duration: test.duration,
|
||||
})),
|
||||
};
|
||||
|
||||
const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.json');
|
||||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
|
||||
console.log(`📄 JSON报告已生成: ${reportPath}`);
|
||||
}
|
||||
|
||||
private formatDuration(ms: number): string {
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
} else if (ms < 60000) {
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
} else {
|
||||
return `${(ms / 60000).toFixed(1)}m`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TestStats {
|
||||
total: number;
|
||||
passed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
flaky: number;
|
||||
passRate: number;
|
||||
failRate: number;
|
||||
skipRate: number;
|
||||
flakyRate: number;
|
||||
totalDuration: number;
|
||||
avgDuration: number;
|
||||
slowestTests: TestCase[];
|
||||
}
|
||||
|
||||
export default CustomReporter;
|
||||
@@ -0,0 +1,87 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('调试创建角色功能', () => {
|
||||
test('调试创建角色流程', async ({ page }) => {
|
||||
const timestamp = Date.now();
|
||||
const roleName = `测试角色_${timestamp}`;
|
||||
const roleKey = `test_role_${timestamp}`;
|
||||
|
||||
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 page.screenshot({ path: 'test-results/debug-1-dialog-opened.png' });
|
||||
});
|
||||
|
||||
await test.step('填写角色信息', async () => {
|
||||
const dialog = page.locator('.el-dialog');
|
||||
|
||||
const roleNameInput = dialog.locator('input').first();
|
||||
await roleNameInput.fill(roleName);
|
||||
await roleNameInput.blur();
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'test-results/debug-2-filled-roleName.png' });
|
||||
|
||||
const roleKeyInput = dialog.locator('input').nth(1);
|
||||
await roleKeyInput.fill(roleKey);
|
||||
await roleKeyInput.blur();
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'test-results/debug-3-filled-roleKey.png' });
|
||||
|
||||
const roleSortInput = dialog.locator('.el-input-number .el-input__inner');
|
||||
await roleSortInput.fill('99');
|
||||
await roleSortInput.blur();
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'test-results/debug-4-filled-roleSort.png' });
|
||||
|
||||
console.log('Filled form with:', { roleName, roleKey, roleSort: 99 });
|
||||
|
||||
const formItems = await dialog.locator('.el-form-item').all();
|
||||
console.log('Number of form items:', formItems.length);
|
||||
|
||||
for (let i = 0; i < formItems.length; i++) {
|
||||
const label = await formItems[i].locator('.el-form-item__label').textContent();
|
||||
const hasError = await formItems[i].locator('.el-form-item__error').isVisible().catch(() => false);
|
||||
if (hasError) {
|
||||
const errorText = await formItems[i].locator('.el-form-item__error').textContent();
|
||||
console.log(`Form item "${label}" has error: ${errorText}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('提交表单', async () => {
|
||||
await page.screenshot({ path: 'test-results/debug-5-before-submit.png' });
|
||||
|
||||
await page.locator('.el-dialog button:has-text("确定")').click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: 'test-results/debug-6-after-submit-1s.png' });
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: 'test-results/debug-7-after-submit-3s.png' });
|
||||
|
||||
const dialogVisible = await page.locator('.el-dialog').isVisible();
|
||||
console.log('Dialog visible after submit:', dialogVisible);
|
||||
|
||||
const successMessage = await page.locator('.el-message--success').isVisible().catch(() => false);
|
||||
console.log('Success message visible:', successMessage);
|
||||
|
||||
const errorMessage = await page.locator('.el-message--error').isVisible().catch(() => false);
|
||||
console.log('Error message visible:', errorMessage);
|
||||
|
||||
if (errorMessage) {
|
||||
const errorText = await page.locator('.el-message--error').textContent();
|
||||
console.log('Error message text:', errorText);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
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.locator('.el-table')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
await test.step('查找测试用户', async () => {
|
||||
const searchInput = page.locator('input[placeholder*="用户名"]').first();
|
||||
await searchInput.fill('testuser_journey');
|
||||
await page.locator('button:has-text("搜索")').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const userRow = page.locator('.el-table__row').first();
|
||||
await expect(userRow).toBeVisible({ timeout: 5000 });
|
||||
await page.screenshot({ path: 'test-results/debug-role-1-user-found.png' });
|
||||
});
|
||||
|
||||
await test.step('打开分配角色对话框', async () => {
|
||||
const userRow = page.locator('.el-table__row').first();
|
||||
await userRow.locator('button:has-text("分配角色")').click();
|
||||
await page.waitForSelector('.el-dialog:has-text("分配角色")', { state: 'visible', timeout: 5000 });
|
||||
await page.screenshot({ path: 'test-results/debug-role-2-dialog-open.png' });
|
||||
});
|
||||
|
||||
await test.step('检查转移组件状态', async () => {
|
||||
const transfer = page.locator('.el-transfer');
|
||||
|
||||
const leftPanel = transfer.locator('.el-transfer-panel').first();
|
||||
const rightPanel = transfer.locator('.el-transfer-panel').last();
|
||||
|
||||
const leftItems = await leftPanel.locator('.el-checkbox').all();
|
||||
const rightItems = await rightPanel.locator('.el-checkbox').all();
|
||||
|
||||
console.log(`左侧面板有 ${leftItems.length} 个选项`);
|
||||
console.log(`右侧面板有 ${rightItems.length} 个选项`);
|
||||
|
||||
for (let i = 0; i < leftItems.length; i++) {
|
||||
const text = await leftItems[i].textContent();
|
||||
const isChecked = await leftItems[i].locator('input').isChecked();
|
||||
console.log(`左侧选项 ${i}: ${text}, 是否选中: ${isChecked}`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < rightItems.length; i++) {
|
||||
const text = await rightItems[i].textContent();
|
||||
const isChecked = await rightItems[i].locator('input').isChecked();
|
||||
console.log(`右侧选项 ${i}: ${text}, 是否选中: ${isChecked}`);
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'test-results/debug-role-3-transfer-state.png' });
|
||||
});
|
||||
|
||||
await test.step('选择并转移角色', async () => {
|
||||
const transfer = page.locator('.el-transfer');
|
||||
|
||||
const superAdminCheckbox = transfer.locator('.el-checkbox:has-text("超级管理员")');
|
||||
const isChecked = await superAdminCheckbox.locator('input').isChecked();
|
||||
|
||||
console.log(`超级管理员复选框是否已选中: ${isChecked}`);
|
||||
|
||||
if (!isChecked) {
|
||||
await superAdminCheckbox.click();
|
||||
await page.waitForTimeout(500);
|
||||
console.log('点击了超级管理员复选框');
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'test-results/debug-role-4-checkbox-clicked.png' });
|
||||
|
||||
const moveToRightButton = transfer.locator('.el-transfer__buttons button').nth(1);
|
||||
const isEnabled = await moveToRightButton.isEnabled();
|
||||
console.log(`转移按钮是否可用: ${isEnabled}`);
|
||||
|
||||
if (isEnabled) {
|
||||
await moveToRightButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
console.log('点击了转移按钮');
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'test-results/debug-role-5-role-transferred.png' });
|
||||
|
||||
const rightPanel = transfer.locator('.el-transfer-panel').last();
|
||||
const rightItems = await rightPanel.locator('.el-checkbox').all();
|
||||
console.log(`转移后右侧面板有 ${rightItems.length} 个选项`);
|
||||
});
|
||||
|
||||
await test.step('点击确定按钮', async () => {
|
||||
const confirmButton = page.locator('.el-dialog:has-text("分配角色") button:has-text("确定")');
|
||||
await confirmButton.click();
|
||||
console.log('点击了确定按钮');
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: 'test-results/debug-role-6-after-confirm.png' });
|
||||
|
||||
const dialog = page.locator('.el-dialog:has-text("分配角色")');
|
||||
const isVisible = await dialog.isVisible();
|
||||
console.log(`对话框是否可见: ${isVisible}`);
|
||||
|
||||
if (isVisible) {
|
||||
const errorMessage = page.locator('.el-message--error');
|
||||
const hasError = await errorMessage.isVisible().catch(() => false);
|
||||
if (hasError) {
|
||||
const errorText = await errorMessage.textContent();
|
||||
console.log(`错误消息: ${errorText}`);
|
||||
}
|
||||
|
||||
const formItems = await dialog.locator('.el-form-item').all();
|
||||
console.log(`表单项数量: ${formItems.length}`);
|
||||
|
||||
for (let i = 0; i < formItems.length; i++) {
|
||||
const hasError = await formItems[i].locator('.el-form-item__error').isVisible().catch(() => false);
|
||||
if (hasError) {
|
||||
const label = await formItems[i].locator('.el-form-item__label').textContent();
|
||||
const errorText = await formItems[i].locator('.el-form-item__error').textContent();
|
||||
console.log(`表单项 "${label}" 有错误: ${errorText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('字典管理功能测试', () => {
|
||||
let authToken: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const response = await request.post('http://localhost:8080/api/auth/login', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
const data = await response.json();
|
||||
authToken = data.token;
|
||||
});
|
||||
|
||||
test('字典管理列表显示测试', async ({ page }) => {
|
||||
await test.step('导航到字典管理页面', async () => {
|
||||
await page.goto('http://localhost:3002/login');
|
||||
|
||||
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
const loginButton = page.locator('button:has-text("登录")').first();
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 点击系统管理菜单
|
||||
const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first();
|
||||
if (await systemMenu.count() > 0) {
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// 点击字典管理
|
||||
const dictManagement = page.locator('.el-menu-item:has-text("字典管理")').first();
|
||||
if (await dictManagement.count() > 0) {
|
||||
await dictManagement.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证字典管理列表显示', async () => {
|
||||
// 检查是否有字典管理列表或表格
|
||||
const tableSelectors = [
|
||||
'table',
|
||||
'.el-table',
|
||||
'[class*="table"]',
|
||||
'.dict-list'
|
||||
];
|
||||
|
||||
let foundTable = false;
|
||||
for (const selector of tableSelectors) {
|
||||
const count = await page.locator(selector).count();
|
||||
if (count > 0) {
|
||||
foundTable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(foundTable).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
export interface TestUser {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface TestRole {
|
||||
roleName: string;
|
||||
roleKey: string;
|
||||
roleSort?: string;
|
||||
status?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface TestMenu {
|
||||
menuName: string;
|
||||
parentId: number;
|
||||
orderNum: number;
|
||||
menuType: string;
|
||||
component?: string;
|
||||
perms?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
type TestData = {
|
||||
adminUser: TestUser;
|
||||
regularUser: TestUser;
|
||||
testRole: TestRole;
|
||||
testMenu: TestMenu;
|
||||
generateTestUser: () => TestUser;
|
||||
generateTestRole: () => TestRole;
|
||||
generateTestMenu: () => TestMenu;
|
||||
};
|
||||
|
||||
export const test = base.extend<TestData>({
|
||||
adminUser: async ({}, use) => {
|
||||
const user: TestUser = {
|
||||
username: 'admin',
|
||||
password: 'password',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138000',
|
||||
};
|
||||
await use(user);
|
||||
},
|
||||
|
||||
regularUser: async ({}, use) => {
|
||||
const user: TestUser = {
|
||||
username: 'testuser',
|
||||
password: 'Test123!@#',
|
||||
email: 'testuser@example.com',
|
||||
phone: '13800138001',
|
||||
};
|
||||
await use(user);
|
||||
},
|
||||
|
||||
testRole: async ({}, use) => {
|
||||
const role: TestRole = {
|
||||
roleName: '测试角色',
|
||||
roleKey: 'test_role',
|
||||
roleSort: '1',
|
||||
status: '1',
|
||||
remark: '测试角色备注',
|
||||
};
|
||||
await use(role);
|
||||
},
|
||||
|
||||
testMenu: async ({}, use) => {
|
||||
const menu: TestMenu = {
|
||||
menuName: '测试菜单',
|
||||
parentId: 0,
|
||||
orderNum: 1,
|
||||
menuType: 'M',
|
||||
component: 'test',
|
||||
perms: 'test:view',
|
||||
status: 1,
|
||||
};
|
||||
await use(menu);
|
||||
},
|
||||
|
||||
generateTestUser: async ({}, use) => {
|
||||
const timestamp = Date.now();
|
||||
const user: TestUser = {
|
||||
username: `testuser_${timestamp}`,
|
||||
password: 'Test123!@#',
|
||||
email: `test_${timestamp}@example.com`,
|
||||
phone: `138${String(timestamp).slice(-8)}`,
|
||||
};
|
||||
await use(() => user);
|
||||
},
|
||||
|
||||
generateTestRole: async ({}, use) => {
|
||||
const timestamp = Date.now();
|
||||
const role: TestRole = {
|
||||
roleName: `测试角色_${timestamp}`,
|
||||
roleKey: `test_role_${timestamp}`,
|
||||
roleSort: '1',
|
||||
status: '1',
|
||||
remark: `测试角色备注_${timestamp}`,
|
||||
};
|
||||
await use(() => role);
|
||||
},
|
||||
|
||||
generateTestMenu: async ({}, use) => {
|
||||
const timestamp = Date.now();
|
||||
const menu: TestMenu = {
|
||||
menuName: `测试菜单_${timestamp}`,
|
||||
parentId: 0,
|
||||
orderNum: 1,
|
||||
menuType: 'M',
|
||||
component: `test_${timestamp}`,
|
||||
perms: `test:view_${timestamp}`,
|
||||
status: 1,
|
||||
};
|
||||
await use(() => menu);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
This is a test file for E2E testing purposes.
|
||||
@@ -0,0 +1,567 @@
|
||||
import { FullConfig } from '@playwright/test';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
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', {
|
||||
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 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 backendHealthy = await checkBackendHealth();
|
||||
const gatewayHealthy = await checkGatewayHealth();
|
||||
const frontendHealthy = await checkFrontendHealth();
|
||||
|
||||
if (!backendHealthy) {
|
||||
console.error('⚠️ 后端服务健康检查失败!');
|
||||
}
|
||||
if (!gatewayHealthy) {
|
||||
console.error('⚠️ 网关服务健康检查失败!');
|
||||
}
|
||||
if (!frontendHealthy) {
|
||||
console.error('⚠️ 前端服务健康检查失败!');
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function stopHealthMonitoring() {
|
||||
if (healthCheckInterval) {
|
||||
clearInterval(healthCheckInterval);
|
||||
healthCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
console.log('🚀 开始全局测试环境设置...');
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.PLAYWRIGHT_HEADLESS = 'false';
|
||||
|
||||
const backendAlreadyRunning = await checkBackendHealth();
|
||||
if (backendAlreadyRunning) {
|
||||
console.log('✅ 后端服务已在运行,跳过启动');
|
||||
} else {
|
||||
const backendDir = path.resolve(__dirname, '../../novalon-manage-api/manage-app');
|
||||
const 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(` 目录: ${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) {
|
||||
console.log(`⚠️ 网关到后端连通性验证失败,状态码: ${response.status},跳过验证继续测试`);
|
||||
// 跳过验证,继续测试
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.token) {
|
||||
console.log('⚠️ 网关到后端连通性验证失败,未返回token,跳过验证继续测试');
|
||||
// 跳过验证,继续测试
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(' ✅ 网关到后端连通性正常');
|
||||
} catch (error) {
|
||||
console.log(`⚠️ 网关到后端连通性验证失败: ${error},跳过验证继续测试`);
|
||||
// 跳过验证,继续测试
|
||||
}
|
||||
|
||||
console.log('✅ 所有服务验证通过');
|
||||
}
|
||||
|
||||
async function waitForBackendReady(): 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: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})`);
|
||||
|
||||
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:8080/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'Test@123'
|
||||
})
|
||||
});
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
console.log('⚠️ 无法登录,跳过数据清理');
|
||||
return;
|
||||
}
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
const token = loginData.token;
|
||||
|
||||
// 获取所有用户
|
||||
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:8080/api/users/${user.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
console.log(` 删除用户: ${user.username}`);
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ 无法删除用户 ${user.username}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有角色
|
||||
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:8080/api/roles/${role.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
console.log(` 删除角色: ${role.roleName}`);
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ 无法删除角色 ${role.roleName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ 测试数据清理完成');
|
||||
} catch (error) {
|
||||
console.log('⚠️ 数据清理失败,继续执行测试');
|
||||
console.error('清理错误:', error);
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
console.log('⚠️ 强制停止后端服务');
|
||||
resolve();
|
||||
}
|
||||
}, 10000);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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('✅ 全局测试环境清理完成');
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
export { globalTeardown };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { globalTeardown } from './global-setup';
|
||||
|
||||
export default globalTeardown;
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
export class TestDataManager {
|
||||
private readonly page: Page;
|
||||
private testData: Map<string, any> = new Map();
|
||||
private cleanupCallbacks: Array<() => Promise<void>> = [];
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
generateUniquePrefix(prefix: string): string {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
return `${prefix}_${timestamp}_${random}`;
|
||||
}
|
||||
|
||||
generateTestEmail(prefix: string = 'test'): string {
|
||||
const uniquePart = this.generateUniquePrefix(prefix);
|
||||
return `${uniquePart}@novalon-test.com`;
|
||||
}
|
||||
|
||||
generateTestUsername(prefix: string = 'testuser'): string {
|
||||
return this.generateUniquePrefix(prefix);
|
||||
}
|
||||
|
||||
generateTestFileName(prefix: string = 'testfile'): string {
|
||||
const uniquePart = this.generateUniquePrefix(prefix);
|
||||
return `${uniquePart}.txt`;
|
||||
}
|
||||
|
||||
generateTestConfigName(prefix: string = 'testconfig'): string {
|
||||
return this.generateUniquePrefix(prefix);
|
||||
}
|
||||
|
||||
generateTestDictName(prefix: string = 'testdict'): string {
|
||||
return this.generateUniquePrefix(prefix);
|
||||
}
|
||||
|
||||
generateTestNotificationTitle(prefix: string = 'testnotify'): string {
|
||||
return this.generateUniquePrefix(prefix);
|
||||
}
|
||||
|
||||
generateTestContent(prefix: string = 'content'): string {
|
||||
const timestamp = new Date().toLocaleString('zh-CN');
|
||||
return `测试内容_${prefix}_${timestamp}`;
|
||||
}
|
||||
|
||||
set(key: string, value: any): void {
|
||||
this.testData.set(key, value);
|
||||
}
|
||||
|
||||
get(key: string): any {
|
||||
return this.testData.get(key);
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this.testData.has(key);
|
||||
}
|
||||
|
||||
remove(key: string): boolean {
|
||||
return this.testData.delete(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.testData.clear();
|
||||
}
|
||||
|
||||
registerCleanup(callback: () => Promise<void>): void {
|
||||
this.cleanupCallbacks.push(callback);
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
console.log('Starting test data cleanup...');
|
||||
|
||||
for (const callback of this.cleanupCallbacks) {
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error('Cleanup callback failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
this.cleanupCallbacks = [];
|
||||
this.testData.clear();
|
||||
console.log('Test data cleanup completed');
|
||||
}
|
||||
|
||||
async cleanupTestConfigs(): Promise<void> {
|
||||
console.log('Cleaning up test configurations...');
|
||||
try {
|
||||
await this.page.goto('/system/config');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
|
||||
const testRows = this.page.locator('.el-table__row').filter({ hasText: 'test' });
|
||||
const count = await testRows.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const row = testRows.nth(i);
|
||||
const deleteButton = row.locator('.el-button--danger').first();
|
||||
|
||||
if (await deleteButton.isVisible()) {
|
||||
await deleteButton.click();
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' });
|
||||
await confirmButton.click();
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Cleaned up ${count} test configurations`);
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup test configurations:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupTestNotifications(): Promise<void> {
|
||||
console.log('Cleaning up test notifications...');
|
||||
try {
|
||||
await this.page.goto('/system/notice');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
|
||||
const testRows = this.page.locator('.el-table__row').filter({ hasText: '测试通知' });
|
||||
const count = await testRows.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const row = testRows.nth(i);
|
||||
const deleteButton = row.locator('.el-button--danger').first();
|
||||
|
||||
if (await deleteButton.isVisible()) {
|
||||
await deleteButton.click();
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' });
|
||||
await confirmButton.click();
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Cleaned up ${count} test notifications`);
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup test notifications:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupTestFiles(): Promise<void> {
|
||||
console.log('Cleaning up test files...');
|
||||
try {
|
||||
await this.page.goto('/files');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
|
||||
const testRows = this.page.locator('.el-table__row').filter({ hasText: 'test' });
|
||||
const count = await testRows.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const row = testRows.nth(i);
|
||||
const deleteButton = row.locator('.el-button--danger').first();
|
||||
|
||||
if (await deleteButton.isVisible()) {
|
||||
await deleteButton.click();
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' });
|
||||
await confirmButton.click();
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Cleaned up ${count} test files`);
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup test files:', error);
|
||||
}
|
||||
}
|
||||
|
||||
createTestFileContent(fileName: string): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
return `Test file created at ${timestamp}\nFilename: ${fileName}\nThis is a test file for E2E testing purposes.`;
|
||||
}
|
||||
|
||||
async setupTestData(): Promise<void> {
|
||||
console.log('Setting up test data...');
|
||||
this.set('setupTime', new Date().toISOString());
|
||||
}
|
||||
|
||||
getTestSummary(): Record<string, any> {
|
||||
return {
|
||||
testDataCount: this.testData.size,
|
||||
cleanupCallbacksCount: this.cleanupCallbacks.length,
|
||||
testDataKeys: Array.from(this.testData.keys()),
|
||||
setupTime: this.get('setupTime'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class TestStabilityHelper {
|
||||
private readonly page: Page;
|
||||
private readonly maxRetries: number = 3;
|
||||
private readonly retryDelay: number = 1000;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async waitForNetworkIdle(timeout: number = 30000): Promise<void> {
|
||||
try {
|
||||
await this.page.waitForLoadState('networkidle', { timeout });
|
||||
} catch (error) {
|
||||
console.log('Network idle timeout, continuing anyway');
|
||||
}
|
||||
}
|
||||
|
||||
async waitForElementVisible(selector: string, timeout: number = 10000): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await expect(element).toBeVisible({ timeout });
|
||||
});
|
||||
}
|
||||
|
||||
async safeClick(selector: string): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await element.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await element.click({ timeout: 5000 });
|
||||
});
|
||||
}
|
||||
|
||||
async safeFill(selector: string, value: string): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await element.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await element.clear();
|
||||
await element.fill(value);
|
||||
});
|
||||
}
|
||||
|
||||
async safeSelect(selector: string, value: string): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await element.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await element.selectOption(value);
|
||||
});
|
||||
}
|
||||
|
||||
async waitForURL(urlPattern: RegExp | string, timeout: number = 30000): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
await this.page.waitForURL(urlPattern, { timeout });
|
||||
});
|
||||
}
|
||||
|
||||
async handleModal(): Promise<void> {
|
||||
try {
|
||||
const modal = this.page.locator('.el-dialog, .el-message-box');
|
||||
const isVisible = await modal.isVisible({ timeout: 2000 });
|
||||
|
||||
if (isVisible) {
|
||||
const confirmButton = modal.locator('.el-button--primary').first();
|
||||
const cancelButton = modal.locator('.el-button--default').first();
|
||||
|
||||
if (await confirmButton.isVisible({ timeout: 1000 })) {
|
||||
await confirmButton.click();
|
||||
} else if (await cancelButton.isVisible({ timeout: 1000 })) {
|
||||
await cancelButton.click();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('No modal found or modal handling failed');
|
||||
}
|
||||
}
|
||||
|
||||
async waitForLoadingComplete(): Promise<void> {
|
||||
try {
|
||||
const loading = this.page.locator('.el-loading-mask, .loading');
|
||||
await loading.waitFor({ state: 'hidden', timeout: 10000 });
|
||||
} catch (error) {
|
||||
console.log('Loading element not found or timeout');
|
||||
}
|
||||
}
|
||||
|
||||
async safeNavigate(url: string): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
await this.page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
|
||||
});
|
||||
}
|
||||
|
||||
async waitForTableData(tableSelector: string, minRows: number = 1): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const table = this.page.locator(tableSelector);
|
||||
await expect(table).toBeVisible({ timeout: 10000 });
|
||||
|
||||
const rows = table.locator('.el-table__row');
|
||||
const rowCount = await rows.count();
|
||||
expect(rowCount).toBeGreaterThanOrEqual(minRows);
|
||||
});
|
||||
}
|
||||
|
||||
async safeScrollIntoView(selector: string): Promise<void> {
|
||||
const element = this.page.locator(selector);
|
||||
await element.scrollIntoViewIfNeeded();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async clearLocalStorage(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
}
|
||||
|
||||
async clearSessionStorage(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
sessionStorage.clear();
|
||||
});
|
||||
}
|
||||
|
||||
async takeScreenshot(name: string): Promise<void> {
|
||||
await this.page.screenshot({ path: `test-results/screenshots/${name}.png`, fullPage: true });
|
||||
}
|
||||
|
||||
async getErrorMessage(): Promise<string | null> {
|
||||
try {
|
||||
const errorElement = this.page.locator('.el-message--error, .error-message');
|
||||
const isVisible = await errorElement.isVisible({ timeout: 2000 });
|
||||
|
||||
if (isVisible) {
|
||||
return await errorElement.textContent();
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async hasErrorMessage(): Promise<boolean> {
|
||||
const errorMessage = await this.getErrorMessage();
|
||||
return errorMessage !== null;
|
||||
}
|
||||
|
||||
private async retry<T>(fn: () => Promise<T>): Promise<T> {
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
console.log(`Attempt ${attempt} failed, retrying...`, error);
|
||||
|
||||
if (attempt < this.maxRetries) {
|
||||
await this.page.waitForTimeout(this.retryDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('All retry attempts failed');
|
||||
}
|
||||
|
||||
async waitForElementNotVisible(selector: string, timeout: number = 10000): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await expect(element).not.toBeVisible({ timeout });
|
||||
});
|
||||
}
|
||||
|
||||
async safeHover(selector: string): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await element.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await element.hover({ timeout: 5000 });
|
||||
});
|
||||
}
|
||||
|
||||
async waitForText(selector: string, text: string, timeout: number = 10000): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await expect(element).toContainText(text, { timeout });
|
||||
});
|
||||
}
|
||||
|
||||
async waitForTextNotPresent(selector: string, text: string, timeout: number = 10000): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await expect(element).not.toContainText(text, { timeout });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,253 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('数据字典管理完整工作流', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
const timestamp = Date.now();
|
||||
const dictType = `test_dict_type_${timestamp}`;
|
||||
const dictName = `测试字典_${timestamp}`;
|
||||
const dictCode = `test_dict_code_${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(/.*dicts/, { timeout: 10000 });
|
||||
});
|
||||
|
||||
await test.step('切换到字典类型标签页', async () => {
|
||||
await page.locator('.el-tabs__item:has-text("字典类型")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
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(dictType);
|
||||
await dialog.locator('input').nth(1).fill(`测试字典类型_${timestamp}`);
|
||||
await dialog.locator('textarea').fill(`这是测试字典类型的备注信息,时间戳:${timestamp}`);
|
||||
});
|
||||
|
||||
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.locator('input[placeholder="请输入字典类型"]').fill(dictType);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const dictTypeRow = page.locator(`tr:has-text("${dictType}")`);
|
||||
await expect(dictTypeRow).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
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 test.step('切换到字典数据标签页', async () => {
|
||||
await page.locator('.el-tabs__item:has-text("字典数据")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
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('.el-select').first().click();
|
||||
await page.locator(`.el-select-dropdown:visible .el-select-dropdown__item:has-text("${dictType}")`).click();
|
||||
|
||||
await dialog.locator('input').nth(1).fill(dictName);
|
||||
await dialog.locator('input').nth(2).fill(dictCode);
|
||||
await dialog.locator('.el-input-number .el-input__inner').fill('99');
|
||||
await dialog.locator('textarea').fill(`这是测试字典数据的备注信息,时间戳:${timestamp}`);
|
||||
});
|
||||
|
||||
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.locator('input[placeholder="请输入字典名称"]').fill(dictName);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const dictDataRow = page.locator(`tr:has-text("${dictName}")`);
|
||||
await expect(dictDataRow).toBeVisible({ timeout: 10000 });
|
||||
await expect(dictDataRow.locator('td').nth(2)).toHaveText(dictCode);
|
||||
});
|
||||
});
|
||||
|
||||
test('编辑字典数据', async ({ page }) => {
|
||||
const updatedName = `更新字典_${timestamp}`;
|
||||
|
||||
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 test.step('切换到字典数据标签页', async () => {
|
||||
await page.locator('.el-tabs__item:has-text("字典数据")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
await test.step('搜索并编辑字典数据', async () => {
|
||||
await page.locator('input[placeholder="请输入字典名称"]').fill(dictName);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const dictDataRow = page.locator(`tr:has-text("${dictName}")`);
|
||||
await dictDataRow.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').nth(1).fill(updatedName);
|
||||
await dialog.locator('textarea').fill(`这是更新后的字典数据备注,时间戳:${timestamp}`);
|
||||
});
|
||||
|
||||
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.locator('input[placeholder="请输入字典名称"]').fill(updatedName);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const dictDataRow = page.locator(`tr:has-text("${updatedName}")`);
|
||||
await expect(dictDataRow).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
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 test.step('切换到字典数据标签页', async () => {
|
||||
await page.locator('.el-tabs__item:has-text("字典数据")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
await test.step('搜索并删除字典数据', async () => {
|
||||
await page.locator('input[placeholder="请输入字典名称"]').fill(`更新字典_${timestamp}`);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const dictDataRow = page.locator(`tr:has-text("更新字典_${timestamp}")`);
|
||||
await dictDataRow.locator('button:has-text("删除")').click();
|
||||
await page.waitForSelector('.el-message-box', { state: 'visible', timeout: 5000 });
|
||||
});
|
||||
|
||||
await test.step('确认删除', async () => {
|
||||
await page.locator('.el-message-box button:has-text("确定")').click();
|
||||
await page.waitForSelector('.el-message-box', { state: 'hidden', timeout: 10000 });
|
||||
await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
await test.step('验证字典数据已删除', async () => {
|
||||
await page.locator('input[placeholder="请输入字典名称"]').fill(`更新字典_${timestamp}`);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const emptyText = page.locator('text=暂无数据');
|
||||
await expect(emptyText).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('字典管理功能验证', async ({ page }) => {
|
||||
await test.step('验证字典管理页面访问权限', async () => {
|
||||
await page.goto('/dicts');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 验证页面标题
|
||||
await expect(page.locator('h1:has-text("数据字典管理")')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// 验证标签页
|
||||
await expect(page.locator('.el-tabs__item:has-text("字典类型")')).toBeVisible();
|
||||
await expect(page.locator('.el-tabs__item:has-text("字典数据")')).toBeVisible();
|
||||
|
||||
// 验证功能按钮
|
||||
await expect(page.locator('button:has-text("新增字典类型")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("新增字典数据")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("查询")')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证字典类型搜索功能', async () => {
|
||||
await page.locator('.el-tabs__item:has-text("字典类型")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const searchInput = page.locator('input[placeholder="请输入字典类型"]');
|
||||
await expect(searchInput).toBeVisible();
|
||||
|
||||
const searchButton = page.locator('button:has-text("查询")');
|
||||
await expect(searchButton).toBeVisible();
|
||||
|
||||
// 测试搜索功能
|
||||
await searchInput.fill('test');
|
||||
await searchButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 验证搜索结果
|
||||
const table = page.locator('.el-table');
|
||||
await expect(table).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证字典数据搜索功能', async () => {
|
||||
await page.locator('.el-tabs__item:has-text("字典数据")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const searchInput = page.locator('input[placeholder="请输入字典名称"]');
|
||||
await expect(searchInput).toBeVisible();
|
||||
|
||||
const searchButton = page.locator('button:has-text("查询")');
|
||||
await expect(searchButton).toBeVisible();
|
||||
|
||||
// 测试搜索功能
|
||||
await searchInput.fill('test');
|
||||
await searchButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 验证搜索结果
|
||||
const table = page.locator('.el-table');
|
||||
await expect(table).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,169 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('系统配置管理完整工作流', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
const timestamp = Date.now();
|
||||
const configKey = `test_config_${timestamp}`;
|
||||
const configName = `测试配置_${timestamp}`;
|
||||
const configValue = `test_value_${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(/.*configs/, { 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(configName);
|
||||
await dialog.locator('input').nth(1).fill(configKey);
|
||||
await dialog.locator('input').nth(2).fill(configValue);
|
||||
await dialog.locator('textarea').fill(`这是测试配置的备注信息,用于验证配置管理功能。时间戳:${timestamp}`);
|
||||
});
|
||||
|
||||
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.locator('input[placeholder="请输入配置名称"]').fill(configName);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const configRow = page.locator(`tr:has-text("${configName}")`);
|
||||
await expect(configRow).toBeVisible({ timeout: 10000 });
|
||||
await expect(configRow.locator('td').nth(1)).toHaveText(configKey);
|
||||
await expect(configRow.locator('td').nth(2)).toHaveText(configValue);
|
||||
});
|
||||
});
|
||||
|
||||
test('编辑系统配置', async ({ page }) => {
|
||||
const updatedValue = `updated_value_${timestamp}`;
|
||||
|
||||
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 test.step('搜索并编辑配置', async () => {
|
||||
await page.locator('input[placeholder="请输入配置名称"]').fill(configName);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const configRow = page.locator(`tr:has-text("${configName}")`);
|
||||
await configRow.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').nth(2).fill(updatedValue);
|
||||
await dialog.locator('textarea').fill(`这是更新后的配置备注,时间戳:${timestamp}`);
|
||||
});
|
||||
|
||||
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.locator('input[placeholder="请输入配置名称"]').fill(configName);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const configRow = page.locator(`tr:has-text("${configName}")`);
|
||||
await expect(configRow.locator('td').nth(2)).toHaveText(updatedValue);
|
||||
});
|
||||
});
|
||||
|
||||
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 test.step('搜索并删除配置', async () => {
|
||||
await page.locator('input[placeholder="请输入配置名称"]').fill(configName);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const configRow = page.locator(`tr:has-text("${configName}")`);
|
||||
await configRow.locator('button:has-text("删除")').click();
|
||||
await page.waitForSelector('.el-message-box', { state: 'visible', timeout: 5000 });
|
||||
});
|
||||
|
||||
await test.step('确认删除', async () => {
|
||||
await page.locator('.el-message-box button:has-text("确定")').click();
|
||||
await page.waitForSelector('.el-message-box', { state: 'hidden', timeout: 10000 });
|
||||
await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
await test.step('验证配置已删除', async () => {
|
||||
await page.locator('input[placeholder="请输入配置名称"]').fill(configName);
|
||||
await page.locator('button:has-text("查询")').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const emptyText = page.locator('text=暂无数据');
|
||||
await expect(emptyText).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('配置管理权限验证', async ({ page }) => {
|
||||
await test.step('验证配置管理页面访问权限', async () => {
|
||||
await page.goto('/configs');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 验证页面标题
|
||||
await expect(page.locator('h1:has-text("系统配置管理")')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// 验证功能按钮可见性
|
||||
await expect(page.locator('button:has-text("新增配置")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("查询")')).toBeVisible();
|
||||
|
||||
// 验证表格列头
|
||||
await expect(page.locator('th:has-text("配置名称")')).toBeVisible();
|
||||
await expect(page.locator('th:has-text("配置键")')).toBeVisible();
|
||||
await expect(page.locator('th:has-text("配置值")')).toBeVisible();
|
||||
await expect(page.locator('th:has-text("操作")')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('验证配置搜索功能', async () => {
|
||||
const searchInput = page.locator('input[placeholder="请输入配置名称"]');
|
||||
await expect(searchInput).toBeVisible();
|
||||
|
||||
const searchButton = page.locator('button:has-text("查询")');
|
||||
await expect(searchButton).toBeVisible();
|
||||
|
||||
// 测试搜索功能
|
||||
await searchInput.fill('test');
|
||||
await searchButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 验证搜索结果
|
||||
const table = page.locator('.el-table');
|
||||
await expect(table).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('菜单管理功能测试', () => {
|
||||
let authToken: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const response = await request.post('http://localhost:8080/api/auth/login', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
const data = await response.json();
|
||||
authToken = data.token;
|
||||
});
|
||||
|
||||
test('菜单列表显示测试', async ({ page }) => {
|
||||
await test.step('导航到菜单管理页面', async () => {
|
||||
await page.goto('http://localhost:3002/login');
|
||||
|
||||
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
|
||||
const passwordInput = page.locator('input[type="password"]').first();
|
||||
const loginButton = page.locator('button:has-text("登录")').first();
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin123');
|
||||
await loginButton.click();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 点击系统管理菜单
|
||||
const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first();
|
||||
if (await systemMenu.count() > 0) {
|
||||
await systemMenu.click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// 点击菜单管理
|
||||
const menuManagement = page.locator('.el-menu-item:has-text("菜单管理")').first();
|
||||
if (await menuManagement.count() > 0) {
|
||||
await menuManagement.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('验证菜单列表显示', async () => {
|
||||
// 检查是否有菜单列表或表格
|
||||
const tableSelectors = [
|
||||
'table',
|
||||
'.el-table',
|
||||
'[class*="table"]',
|
||||
'.menu-list'
|
||||
];
|
||||
|
||||
let foundTable = false;
|
||||
for (const selector of tableSelectors) {
|
||||
const count = await page.locator(selector).count();
|
||||
if (count > 0) {
|
||||
foundTable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(foundTable).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
export class DashboardPage {
|
||||
readonly page: Page;
|
||||
readonly userInfo: Locator;
|
||||
readonly userManagementLink: Locator;
|
||||
readonly roleManagementLink: Locator;
|
||||
readonly menuManagementLink: Locator;
|
||||
readonly systemConfigLink: Locator;
|
||||
readonly noticeManagementLink: Locator;
|
||||
readonly fileManagementLink: Locator;
|
||||
readonly operationLogLink: Locator;
|
||||
readonly loginLogLink: Locator;
|
||||
readonly dictionaryLink: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.userInfo = page.locator('.el-avatar');
|
||||
this.userManagementLink = page.locator('.el-menu-item:has-text("用户管理")');
|
||||
this.roleManagementLink = page.locator('.el-menu-item:has-text("角色管理")');
|
||||
this.menuManagementLink = page.locator('.el-menu-item:has-text("菜单管理")');
|
||||
this.systemConfigLink = page.locator('.el-menu-item:has-text("参数配置")');
|
||||
this.noticeManagementLink = page.locator('.el-menu-item:has-text("通知公告")');
|
||||
this.fileManagementLink = page.locator('.el-menu-item:has-text("文件列表")');
|
||||
this.operationLogLink = page.locator('.el-menu-item:has-text("操作日志")');
|
||||
this.loginLogLink = page.locator('.el-menu-item:has-text("登录日志")');
|
||||
this.dictionaryLink = page.locator('.el-menu-item:has-text("字典管理")');
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/dashboard');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async navigateToUserManagement() {
|
||||
const systemMenu = this.page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.userManagementLink.click();
|
||||
await this.page.waitForURL('**/users', { timeout: 30000 });
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async navigateToRoleManagement() {
|
||||
const systemMenu = this.page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.roleManagementLink.click();
|
||||
await this.page.waitForURL('**/roles', { timeout: 30000 });
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async navigateToMenuManagement() {
|
||||
const systemMenu = this.page.locator('.el-sub-menu__title:has-text("系统管理")');
|
||||
await systemMenu.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.menuManagementLink.click();
|
||||
await this.page.waitForURL('**/menus', { timeout: 30000 });
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async navigateToSystemConfig() {
|
||||
const configMenu = this.page.locator('.el-sub-menu__title:has-text("系统配置")');
|
||||
await configMenu.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.systemConfigLink.click();
|
||||
await this.page.waitForURL('**/sys/config', { timeout: 30000 });
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async navigateToNoticeManagement() {
|
||||
const notifyMenu = this.page.locator('.el-sub-menu__title:has-text("通知中心")');
|
||||
await notifyMenu.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.noticeManagementLink.click();
|
||||
await this.page.waitForURL('**/notice', { timeout: 30000 });
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async navigateToFileManagement() {
|
||||
const fileMenu = this.page.locator('.el-sub-menu__title:has-text("文件管理")');
|
||||
await fileMenu.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.fileManagementLink.click();
|
||||
await this.page.waitForURL('**/files', { timeout: 30000 });
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async navigateToAudit() {
|
||||
const auditMenu = this.page.locator('.el-sub-menu__title:has-text("审计中心")');
|
||||
await auditMenu.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async navigateToOperationLog() {
|
||||
await this.navigateToAudit();
|
||||
await this.operationLogLink.click();
|
||||
await this.page.waitForURL('**/oplog', { timeout: 30000 });
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async navigateToLoginLog() {
|
||||
await this.navigateToAudit();
|
||||
await this.loginLogLink.click();
|
||||
await this.page.waitForURL('**/loginlog', { timeout: 30000 });
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async navigateToNotification() {
|
||||
const notifyMenu = this.page.locator('.el-sub-menu__title:has-text("通知中心")');
|
||||
await notifyMenu.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.noticeManagementLink.click();
|
||||
await this.page.waitForURL('**/notification', { timeout: 30000 });
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async navigateToDictionary() {
|
||||
const configMenu = this.page.locator('.el-sub-menu__title:has-text("系统配置")');
|
||||
await configMenu.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.dictionaryLink.click();
|
||||
await this.page.waitForURL('**/dict', { timeout: 30000 });
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async getUsername(): Promise<string | null> {
|
||||
return await this.userInfo.textContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Page, Locator, expect } from '@playwright/test';
|
||||
|
||||
export class DictionaryManagementPage {
|
||||
readonly page: Page;
|
||||
readonly table: 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');
|
||||
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() {
|
||||
try {
|
||||
console.log('导航到字典管理页面...');
|
||||
await this.page.goto('/dict');
|
||||
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await expect(this.page).toHaveURL(/.*dict/);
|
||||
|
||||
console.log('字典管理页面加载完成');
|
||||
} catch (error) {
|
||||
await this.page.screenshot({ path: `test-results/dict-management-error-${Date.now()}.png` });
|
||||
console.error('导航到字典管理页面失败:', error);
|
||||
throw new Error(`导航到字典管理页面失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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);
|
||||
|
||||
const confirmBtn = this.page.locator('.el-message-box').getByRole('button', { name: '确定' });
|
||||
await confirmBtn.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Page, Locator, expect } from '@playwright/test';
|
||||
|
||||
export class ExceptionLogPage {
|
||||
readonly page: Page;
|
||||
readonly table: Locator;
|
||||
readonly searchInput: Locator;
|
||||
readonly searchButton: Locator;
|
||||
readonly exportButton: Locator;
|
||||
readonly refreshButton: Locator;
|
||||
readonly detailButton: Locator;
|
||||
readonly successMessage: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.locator('.el-table').or(page.locator('.exception-log-table'));
|
||||
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.exportButton = page.getByRole('button', { name: '导出' }).or(page.locator('button:has-text("导出")'));
|
||||
this.refreshButton = page.getByRole('button', { name: '刷新' }).or(page.locator('button:has-text("刷新")'));
|
||||
this.detailButton = page.getByRole('button', { name: '详情' }).or(page.locator('.detail-button'));
|
||||
this.successMessage = page.locator('.el-message--success').or(page.locator('.success-message'));
|
||||
}
|
||||
|
||||
async goto() {
|
||||
try {
|
||||
console.log('导航到异常日志页面...');
|
||||
await this.page.goto('/exceptionlog');
|
||||
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await expect(this.page).toHaveURL(/.*exceptionlog/);
|
||||
|
||||
console.log('异常日志页面加载完成');
|
||||
} catch (error) {
|
||||
await this.page.screenshot({ path: `test-results/exception-log-error-${Date.now()}.png` });
|
||||
console.error('导航到异常日志页面失败:', error);
|
||||
throw new Error(`导航到异常日志页面失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async search(keyword: string) {
|
||||
await this.searchInput.fill(keyword);
|
||||
await this.searchButton.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async clearSearch() {
|
||||
await this.searchInput.fill('');
|
||||
await this.searchButton.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async exportData() {
|
||||
await this.exportButton.click();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.refreshButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async viewDetail(exceptionId: string) {
|
||||
const exceptionRow = this.table.locator('tbody tr').filter({ hasText: exceptionId });
|
||||
await exceptionRow.locator('.detail-button').or(this.page.getByRole('button', { name: '详情' })).click();
|
||||
}
|
||||
|
||||
async closeDetailDialog() {
|
||||
await this.page.getByRole('button', { name: '关闭' }).or(this.page.locator('.el-dialog .close-button')).click();
|
||||
}
|
||||
|
||||
async containsText(text: string): Promise<boolean> {
|
||||
return await this.table.getByText(text).count() > 0;
|
||||
}
|
||||
|
||||
async getTableRowCount(): Promise<number> {
|
||||
return await this.table.locator('tbody tr').count();
|
||||
}
|
||||
|
||||
async isSuccessMessageVisible(): Promise<boolean> {
|
||||
try {
|
||||
return await this.successMessage.isVisible({ timeout: 3000 });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async reload() {
|
||||
await this.page.reload();
|
||||
}
|
||||
|
||||
async verifyTableContains(text: string): Promise<void> {
|
||||
const contains = await this.containsText(text);
|
||||
if (!contains) {
|
||||
throw new Error(`Table does not contain text: ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getLogCount(): Promise<number> {
|
||||
return await this.table.locator('tbody tr').count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class FileManagementPage {
|
||||
readonly page: Page;
|
||||
readonly uploadButton;
|
||||
readonly fileInput;
|
||||
readonly table;
|
||||
readonly deleteButton;
|
||||
readonly downloadButton;
|
||||
readonly searchInput;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.uploadButton = page.locator('.el-upload--text').first();
|
||||
this.fileInput = page.locator('input[type="file"]');
|
||||
this.table = page.locator('.el-table');
|
||||
this.deleteButton = page.getByRole('button', { name: '删除' });
|
||||
this.downloadButton = page.getByRole('button', { name: '下载' });
|
||||
this.searchInput = page.locator('.search-bar .el-input__inner');
|
||||
}
|
||||
|
||||
async goto() {
|
||||
try {
|
||||
console.log('导航到文件管理页面...');
|
||||
await this.page.goto('/files');
|
||||
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await expect(this.page).toHaveURL(/.*files/);
|
||||
|
||||
console.log('文件管理页面加载完成');
|
||||
} catch (error) {
|
||||
await this.page.screenshot({ path: `test-results/file-management-error-${Date.now()}.png` });
|
||||
console.error('导航到文件管理页面失败:', error);
|
||||
throw new Error(`导航到文件管理页面失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(filePath: string) {
|
||||
await this.uploadButton.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await this.uploadButton.click();
|
||||
|
||||
const fileInput = this.page.locator('input[type="file"]');
|
||||
await fileInput.setInputFiles(filePath);
|
||||
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async deleteFile(fileName: string) {
|
||||
const row = this.table.locator('tr').filter({ hasText: fileName }).first();
|
||||
await row.locator('.el-button--danger').click();
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' });
|
||||
await confirmButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async downloadFile(fileName: string) {
|
||||
const row = this.table.locator('tr').filter({ hasText: fileName }).first();
|
||||
const downloadButton = row.locator('.el-button--primary').first();
|
||||
await downloadButton.click();
|
||||
}
|
||||
|
||||
async searchFile(keyword: string) {
|
||||
await this.searchInput.fill(keyword);
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async clearSearch() {
|
||||
await this.searchInput.clear();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
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 clickUploadButton() {
|
||||
await this.uploadButton.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await this.uploadButton.click();
|
||||
}
|
||||
|
||||
async submitUpload() {
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-dialog .el-button--primary'));
|
||||
await confirmButton.click();
|
||||
}
|
||||
|
||||
async clickDeleteButton(rowNumber: number) {
|
||||
const row = this.table.locator(`tbody tr:nth-child(${rowNumber})`);
|
||||
await row.locator('.el-button--danger').click();
|
||||
}
|
||||
|
||||
async clickDownloadButton(rowNumber: number) {
|
||||
const row = this.table.locator(`tbody tr:nth-child(${rowNumber})`);
|
||||
await row.locator('.el-button--primary').first().click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class LoginLogPage {
|
||||
readonly page: Page;
|
||||
readonly searchInput;
|
||||
readonly searchButton;
|
||||
readonly table;
|
||||
readonly exportButton;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.searchInput = page.getByPlaceholder('搜索用户名或IP地址');
|
||||
this.searchButton = page.getByRole('button', { name: '搜索' });
|
||||
this.table = page.locator('.el-table');
|
||||
this.exportButton = page.getByRole('button', { name: '导出' });
|
||||
}
|
||||
|
||||
async goto() {
|
||||
try {
|
||||
console.log('导航到登录日志页面...');
|
||||
await this.page.goto('/loginlog');
|
||||
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await expect(this.page).toHaveURL(/.*loginlog/);
|
||||
|
||||
console.log('登录日志页面加载完成');
|
||||
} catch (error) {
|
||||
await this.page.screenshot({ path: `test-results/login-log-error-${Date.now()}.png` });
|
||||
console.error('导航到登录日志页面失败:', error);
|
||||
throw new Error(`导航到登录日志页面失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async searchByKeyword(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 exportData() {
|
||||
await this.exportButton.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
export class LoginPage {
|
||||
readonly page: Page;
|
||||
readonly usernameInput: Locator;
|
||||
readonly passwordInput: Locator;
|
||||
readonly loginButton: Locator;
|
||||
readonly errorMessage: Locator;
|
||||
readonly logoutButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.usernameInput = page.locator('input[placeholder="请输入用户名"]');
|
||||
this.passwordInput = page.locator('input[placeholder="请输入密码"]');
|
||||
this.loginButton = page.locator('button:has-text("登录")');
|
||||
this.errorMessage = page.locator('.el-message--error .el-message__content');
|
||||
this.logoutButton = page.getByRole('button', { name: '退出登录' });
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/login');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async login(username: string, password: string, maxRetries: number = 3) {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
console.log(`Login attempt ${attempt}/${maxRetries}`);
|
||||
|
||||
try {
|
||||
await this.usernameInput.fill(username);
|
||||
await this.passwordInput.fill(password);
|
||||
console.log('Filled username and password');
|
||||
|
||||
await this.loginButton.click();
|
||||
console.log('Clicked login button');
|
||||
|
||||
await this.page.waitForURL(/\/(dashboard|\/)$/, { timeout: 30000 });
|
||||
console.log('Successfully navigated to dashboard or home');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
console.log('Network idle achieved');
|
||||
await this.page.waitForTimeout(2000);
|
||||
console.log('Login completed successfully');
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
console.log(`Login attempt ${attempt} failed:`, error);
|
||||
|
||||
const currentUrl = this.page.url();
|
||||
console.log('Current URL:', currentUrl);
|
||||
|
||||
const errorMessage = await this.getErrorMessage();
|
||||
if (errorMessage) {
|
||||
console.log('Login error message:', errorMessage);
|
||||
}
|
||||
|
||||
const token = await this.page.evaluate(() => localStorage.getItem('token'));
|
||||
console.log('Token in localStorage:', token ? 'exists' : 'not found');
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`Waiting 2 seconds before retry...`);
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
await this.goto();
|
||||
console.log('Navigated back to login page for retry');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`All ${maxRetries} login attempts failed`);
|
||||
throw lastError || new Error('Login failed after all retries');
|
||||
}
|
||||
|
||||
async getErrorMessage(): Promise<string | null> {
|
||||
try {
|
||||
await this.page.waitForSelector('.el-message--error', { timeout: 10000 });
|
||||
await this.page.waitForTimeout(500);
|
||||
const messageElement = await this.page.locator('.el-message--error .el-message__content').first();
|
||||
const text = await messageElement.textContent();
|
||||
return text;
|
||||
} catch {
|
||||
try {
|
||||
await this.page.waitForSelector('.el-message', { timeout: 5000 });
|
||||
await this.page.waitForTimeout(500);
|
||||
const messageElement = await this.page.locator('.el-message .el-message__content').first();
|
||||
const text = await messageElement.textContent();
|
||||
return text;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
const avatar = this.page.locator('.el-avatar');
|
||||
await avatar.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
const logoutButton = this.page.locator('.el-dropdown-menu').getByText('退出登录');
|
||||
await logoutButton.click();
|
||||
await this.page.waitForURL('**/login', { timeout: 10000 });
|
||||
}
|
||||
|
||||
async isLoggedIn(): Promise<boolean> {
|
||||
return this.page.url().includes('/dashboard') || this.page.url() === this.page.url().split('?')[0].split('#')[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Page, Locator, expect } from '@playwright/test';
|
||||
|
||||
export class MenuManagementPage {
|
||||
readonly page: Page;
|
||||
readonly table: Locator;
|
||||
readonly createMenuButton: Locator;
|
||||
readonly searchInput: Locator;
|
||||
readonly searchButton: Locator;
|
||||
readonly successMessage: Locator;
|
||||
readonly treeContainer: Locator;
|
||||
readonly expandAllButton: Locator;
|
||||
readonly collapseAllButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.locator('.el-table').or(page.locator('.menu-table'));
|
||||
this.createMenuButton = 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.treeContainer = page.locator('.el-tree').or(page.locator('.menu-tree'));
|
||||
this.expandAllButton = page.getByRole('button', { name: '展开全部' }).or(page.locator('button:has-text("展开全部")'));
|
||||
this.collapseAllButton = page.getByRole('button', { name: '折叠全部' }).or(page.locator('button:has-text("折叠全部")'));
|
||||
}
|
||||
|
||||
async goto() {
|
||||
try {
|
||||
console.log('导航到菜单管理页面...');
|
||||
await this.page.goto('/menus');
|
||||
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
|
||||
await this.page.waitForSelector('.el-tree', { timeout: 10000 }).catch(() => {
|
||||
return this.page.waitForSelector('.el-table', { timeout: 5000 });
|
||||
});
|
||||
|
||||
await expect(this.page).toHaveURL(/.*menus/);
|
||||
|
||||
console.log('菜单管理页面加载完成');
|
||||
} catch (error) {
|
||||
await this.page.screenshot({ path: `test-results/menu-management-error-${Date.now()}.png` });
|
||||
console.error('导航到菜单管理页面失败:', error);
|
||||
throw new Error(`导航到菜单管理页面失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async clickCreateMenu() {
|
||||
await this.createMenuButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async fillMenuForm(menuData: {
|
||||
menuName: string;
|
||||
menuType?: string;
|
||||
path?: string;
|
||||
component?: string;
|
||||
permission?: string;
|
||||
sort?: number;
|
||||
visible?: string;
|
||||
status?: string;
|
||||
}) {
|
||||
const dialog = this.page.locator('.el-dialog');
|
||||
|
||||
await dialog.locator('input').first().fill(menuData.menuName);
|
||||
|
||||
if (menuData.menuType) {
|
||||
const menuTypeSelect = dialog.locator('.el-select').first();
|
||||
await menuTypeSelect.click();
|
||||
await this.page.waitForTimeout(300);
|
||||
await this.page.getByRole('option', { name: menuData.menuType }).click();
|
||||
}
|
||||
|
||||
if (menuData.path) {
|
||||
const pathInput = dialog.locator('input[placeholder*="路径"]');
|
||||
if (await pathInput.count() > 0) {
|
||||
await pathInput.fill(menuData.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (menuData.component) {
|
||||
const componentInput = dialog.locator('input[placeholder*="组件"]');
|
||||
if (await componentInput.count() > 0) {
|
||||
await componentInput.fill(menuData.component);
|
||||
}
|
||||
}
|
||||
|
||||
if (menuData.permission) {
|
||||
const permissionInput = dialog.locator('input[placeholder*="权限"]');
|
||||
if (await permissionInput.count() > 0) {
|
||||
await permissionInput.fill(menuData.permission);
|
||||
}
|
||||
}
|
||||
|
||||
if (menuData.sort !== undefined) {
|
||||
const sortInput = dialog.locator('input[type="number"]');
|
||||
if (await sortInput.count() > 0) {
|
||||
await sortInput.fill(String(menuData.sort));
|
||||
}
|
||||
}
|
||||
|
||||
if (menuData.visible) {
|
||||
const visibleRadio = dialog.locator(`input[value="${menuData.visible}"]`);
|
||||
if (await visibleRadio.count() > 0) {
|
||||
await visibleRadio.check();
|
||||
}
|
||||
}
|
||||
|
||||
if (menuData.status) {
|
||||
const statusRadio = dialog.locator(`input[value="${menuData.status}"]`);
|
||||
if (await statusRadio.count() > 0) {
|
||||
await statusRadio.check();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async submitForm() {
|
||||
await this.page.getByRole('button', { name: '确定' }).or(this.page.locator('button:has-text("确定")')).click();
|
||||
}
|
||||
|
||||
async editMenu(menuName: string) {
|
||||
const menuRow = this.table.locator('tbody tr').filter({ hasText: menuName });
|
||||
await menuRow.getByRole('button', { name: '编辑' }).or(this.page.locator('.edit-button')).click();
|
||||
}
|
||||
|
||||
async deleteMenu(menuName: string) {
|
||||
const menuRow = this.table.locator('tbody tr').filter({ hasText: menuName });
|
||||
await menuRow.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 expandAll() {
|
||||
await this.expandAllButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async collapseAll() {
|
||||
await this.collapseAllButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
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 getMenuCount(): Promise<number> {
|
||||
return await this.table.locator('tbody tr').count();
|
||||
}
|
||||
|
||||
async reload() {
|
||||
await this.page.reload();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class NotificationPage {
|
||||
readonly page: Page;
|
||||
readonly table;
|
||||
readonly addButton;
|
||||
readonly saveButton;
|
||||
readonly cancelButton;
|
||||
readonly dialog;
|
||||
readonly titleInput;
|
||||
readonly contentInput;
|
||||
readonly noticeTypeSelect;
|
||||
readonly statusSelect;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.locator('.el-table');
|
||||
this.addButton = page.getByRole('button', { name: '新增公告' });
|
||||
this.saveButton = page.getByRole('button', { name: '确定' });
|
||||
this.cancelButton = page.getByRole('button', { name: '取消' });
|
||||
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() {
|
||||
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) {
|
||||
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();
|
||||
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();
|
||||
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 getTableRowCount() {
|
||||
const rows = await this.table.locator('.el-table__row').count();
|
||||
return rows;
|
||||
}
|
||||
|
||||
async verifyTableContains(text: string) {
|
||||
await expect(this.table).toContainText(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class OperationLogPage {
|
||||
readonly page: Page;
|
||||
readonly searchInput;
|
||||
readonly searchButton;
|
||||
readonly table;
|
||||
readonly exportButton;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.searchInput = page.getByPlaceholder('搜索操作人或操作模块');
|
||||
this.searchButton = page.getByRole('button', { name: '搜索' });
|
||||
this.table = page.locator('.el-table');
|
||||
this.exportButton = page.getByRole('button', { name: '导出' });
|
||||
}
|
||||
|
||||
async goto() {
|
||||
try {
|
||||
console.log('导航到操作日志页面...');
|
||||
await this.page.goto('/oplog');
|
||||
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await expect(this.page).toHaveURL(/.*oplog/);
|
||||
|
||||
console.log('操作日志页面加载完成');
|
||||
} catch (error) {
|
||||
await this.page.screenshot({ path: `test-results/operation-log-error-${Date.now()}.png` });
|
||||
console.error('导航到操作日志页面失败:', error);
|
||||
throw new Error(`导航到操作日志页面失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async searchByKeyword(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 exportData() {
|
||||
await this.exportButton.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import { Page, Locator, expect } from '@playwright/test';
|
||||
|
||||
export class RoleManagementPage {
|
||||
readonly page: Page;
|
||||
readonly table: Locator;
|
||||
readonly createRoleButton: Locator;
|
||||
readonly successMessage: Locator;
|
||||
readonly roleNameInput: Locator;
|
||||
readonly roleKeyInput: Locator;
|
||||
readonly roleSortInput: Locator;
|
||||
readonly statusSelect: Locator;
|
||||
readonly remarkInput: Locator;
|
||||
readonly permissionDialog: Locator;
|
||||
readonly savePermissionButton: Locator;
|
||||
readonly searchInput: Locator;
|
||||
readonly searchButton: Locator;
|
||||
readonly pagination: Locator;
|
||||
readonly nextPageButton: Locator;
|
||||
readonly prevPageButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.locator('.el-table').first();
|
||||
this.createRoleButton = page.getByRole('button', { name: '新增角色' }).or(page.locator('button:has-text("新增角色")'));
|
||||
this.successMessage = page.locator('.el-message--success').or(page.locator('.success-message'));
|
||||
this.roleNameInput = page.locator('input[placeholder*="角色名称"]').or(page.locator('input[name*="roleName"]'));
|
||||
this.roleKeyInput = page.locator('input[placeholder*="角色权限字符串"]').or(page.locator('input[name*="roleKey"]'));
|
||||
this.roleSortInput = page.locator('input[placeholder*="显示顺序"]').or(page.locator('input[name*="roleSort"]'));
|
||||
this.statusSelect = page.locator('select[name*="status"]').or(page.locator('.el-select'));
|
||||
this.remarkInput = page.locator('textarea[placeholder*="备注"]').or(page.locator('textarea[name*="remark"]'));
|
||||
this.permissionDialog = page.locator('.permission-dialog').or(page.locator('.el-dialog'));
|
||||
this.savePermissionButton = page.getByRole('button', { name: '保存' }).or(page.locator('.permission-dialog .save-button'));
|
||||
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.pagination = page.locator('.el-pagination').or(page.locator('.pagination'));
|
||||
this.nextPageButton = page.locator('.el-pagination .btn-next').or(page.locator('.pagination .next-page'));
|
||||
this.prevPageButton = page.locator('.el-pagination .btn-prev').or(page.locator('.pagination .prev-page'));
|
||||
}
|
||||
|
||||
async goto() {
|
||||
try {
|
||||
console.log('导航到角色管理页面...');
|
||||
await this.page.goto('/roles');
|
||||
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await expect(this.page).toHaveURL(/.*roles/);
|
||||
|
||||
console.log('角色管理页面加载完成');
|
||||
} catch (error) {
|
||||
await this.page.screenshot({ path: `test-results/role-management-error-${Date.now()}.png` });
|
||||
console.error('导航到角色管理页面失败:', error);
|
||||
throw new Error(`导航到角色管理页面失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async waitForTableReady() {
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
|
||||
await this.page.waitForFunction(
|
||||
() => {
|
||||
const rows = document.querySelectorAll('.el-table__body-wrapper tbody tr');
|
||||
return rows.length > 0;
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
).catch(() => {
|
||||
console.log('表格没有数据,继续执行');
|
||||
});
|
||||
}
|
||||
|
||||
async clickCreateRole() {
|
||||
await this.createRoleButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async fillRoleForm(roleData: {
|
||||
roleName: string;
|
||||
roleKey: string;
|
||||
roleSort?: string;
|
||||
status?: string;
|
||||
remark?: string;
|
||||
}) {
|
||||
await this.page.locator('.el-dialog').locator('input').first().fill(roleData.roleName);
|
||||
await this.page.locator('.el-dialog').locator('input').nth(1).fill(roleData.roleKey);
|
||||
|
||||
if (roleData.roleSort) {
|
||||
const sortInput = this.page.locator('.el-dialog').locator('.el-input-number');
|
||||
if (await sortInput.count() > 0) {
|
||||
const input = sortInput.locator('input');
|
||||
await input.fill(roleData.roleSort);
|
||||
}
|
||||
}
|
||||
|
||||
if (roleData.status) {
|
||||
const statusSelect = this.page.locator('.el-dialog').locator('.el-form-item').filter({ hasText: '状态' }).locator('.el-select');
|
||||
if (await statusSelect.count() > 0) {
|
||||
await statusSelect.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const statusText = roleData.status === 'ACTIVE' ? '正常' : '禁用';
|
||||
const dropdown = this.page.locator('.el-select-dropdown');
|
||||
if (await dropdown.count() > 0) {
|
||||
const options = dropdown.locator('.el-select-dropdown__item');
|
||||
const optionCount = await options.count();
|
||||
|
||||
for (let i = 0; i < optionCount; i++) {
|
||||
const optionText = await options.nth(i).textContent();
|
||||
if (optionText && optionText.includes(statusText)) {
|
||||
await options.nth(i).click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
|
||||
if (roleData.remark) {
|
||||
await this.page.locator('.el-dialog').locator('textarea').fill(roleData.remark);
|
||||
}
|
||||
}
|
||||
|
||||
async submitForm() {
|
||||
const dialog = this.page.locator('.el-dialog');
|
||||
const submitButton = dialog.getByRole('button', { name: '确定' }).or(dialog.locator('button:has-text("确定")'));
|
||||
|
||||
await submitButton.click();
|
||||
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async waitForSuccessMessage(timeout: number = 10000): Promise<boolean> {
|
||||
try {
|
||||
const message = this.page.locator('.el-message--success').or(this.page.locator('.el-message'));
|
||||
await message.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('等待成功消息超时,检查是否有错误消息');
|
||||
|
||||
try {
|
||||
const errorMessage = this.page.locator('.el-message--error').or(this.page.locator('.el-message--warning'));
|
||||
if (await errorMessage.count() > 0) {
|
||||
const errorText = await errorMessage.first().textContent();
|
||||
console.log('发现错误消息:', errorText);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('没有发现错误消息');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async editRole(rowNumber: number) {
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '编辑' }).or(this.page.locator(`tbody tr:nth-child(${rowNumber}) .edit-button`)).click();
|
||||
}
|
||||
|
||||
async deleteRole(rowNumber: number) {
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '删除' }).or(this.page.locator(`tbody tr:nth-child(${rowNumber}) .delete-button`)).click();
|
||||
}
|
||||
|
||||
async confirmDelete() {
|
||||
await this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.confirm-dialog .confirm-button')).click();
|
||||
}
|
||||
|
||||
async openPermissionDialog(rowNumber: number) {
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '权限' }).or(this.page.locator(`tbody tr:nth-child(${rowNumber}) .permission-button`)).click();
|
||||
}
|
||||
|
||||
async selectPermission(permissionValue: string) {
|
||||
await this.page.click(`input[type="checkbox"][value="${permissionValue}"]`);
|
||||
}
|
||||
|
||||
async savePermissions() {
|
||||
await this.savePermissionButton.click();
|
||||
}
|
||||
|
||||
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 reload() {
|
||||
await this.page.reload();
|
||||
}
|
||||
|
||||
async getRoleName(rowNumber: number): Promise<string | null> {
|
||||
return await this.table.locator(`tbody tr:nth-child(${rowNumber}) td:first-child`).textContent();
|
||||
}
|
||||
|
||||
async clickPermissionButton(rowNumber: number) {
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '权限' }).or(this.page.locator(`tbody tr:nth-child(${rowNumber}) .permission-button`)).click();
|
||||
}
|
||||
|
||||
async deselectPermission(permissionValue: string) {
|
||||
const checkbox = this.page.locator(`input[type="checkbox"][value="${permissionValue}"]`);
|
||||
if (await checkbox.isChecked()) {
|
||||
await checkbox.click();
|
||||
}
|
||||
}
|
||||
|
||||
async search(keyword: string) {
|
||||
await this.searchInput.fill(keyword);
|
||||
await this.searchButton.click();
|
||||
}
|
||||
|
||||
async clearSearch() {
|
||||
await this.searchInput.fill('');
|
||||
await this.searchButton.click();
|
||||
}
|
||||
|
||||
async clickStatusButton(rowNumber: number) {
|
||||
const row = this.table.locator(`tbody tr:nth-child(${rowNumber})`);
|
||||
await row.locator('.el-button--text').filter({ hasText: /状态|启用|禁用/ }).first().click();
|
||||
}
|
||||
|
||||
async getCurrentPage(): Promise<string> {
|
||||
try {
|
||||
const activePage = this.page.locator('.el-pager li.is-active');
|
||||
if (await activePage.count() > 0) {
|
||||
return await activePage.textContent() || '1';
|
||||
}
|
||||
|
||||
const currentPage = this.page.locator('.el-pagination__current');
|
||||
if (await currentPage.count() > 0) {
|
||||
return await currentPage.textContent() || '1';
|
||||
}
|
||||
|
||||
return '1';
|
||||
} catch (error) {
|
||||
console.log('获取当前页码失败,返回默认值');
|
||||
return '1';
|
||||
}
|
||||
}
|
||||
|
||||
async nextPage() {
|
||||
await this.nextPageButton.click();
|
||||
}
|
||||
|
||||
async prevPage() {
|
||||
await this.prevPageButton.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class SystemConfigPage {
|
||||
readonly page: Page;
|
||||
readonly table;
|
||||
readonly addButton;
|
||||
readonly saveButton;
|
||||
readonly cancelButton;
|
||||
readonly dialog;
|
||||
readonly configNameInput;
|
||||
readonly configKeyInput;
|
||||
readonly configValueInput;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.locator('.el-table');
|
||||
this.addButton = page.getByRole('button', { name: '新增配置' });
|
||||
this.saveButton = page.getByRole('button', { name: '确定' });
|
||||
this.cancelButton = page.getByRole('button', { name: '取消' });
|
||||
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() {
|
||||
try {
|
||||
console.log('导航到系统配置页面...');
|
||||
await this.page.goto('/sys/config');
|
||||
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await expect(this.page).toHaveURL(/.*config/);
|
||||
|
||||
console.log('系统配置页面加载完成');
|
||||
} catch (error) {
|
||||
await this.page.screenshot({ path: `test-results/system-config-error-${Date.now()}.png` });
|
||||
console.error('导航到系统配置页面失败:', error);
|
||||
throw new Error(`导航到系统配置页面失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
await this.configValueInput.fill(configValue);
|
||||
|
||||
await this.saveButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async editConfig(configKey: string, newValue: string) {
|
||||
const row = this.table.locator('tr').filter({ hasText: configKey }).first();
|
||||
const editBtn = row.getByRole('button', { name: '编辑' });
|
||||
await editBtn.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.configValueInput.clear();
|
||||
await this.configValueInput.fill(newValue);
|
||||
|
||||
await this.saveButton.click();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async deleteConfig(configKey: string) {
|
||||
const row = this.table.locator('tr').filter({ hasText: configKey }).first();
|
||||
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 getTableRowCount() {
|
||||
const rows = await this.table.locator('.el-table__row').count();
|
||||
return rows;
|
||||
}
|
||||
|
||||
async verifyTableContains(text: string) {
|
||||
await expect(this.table).toContainText(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import { Page, Locator, expect } from '@playwright/test';
|
||||
|
||||
export class UserManagementPage {
|
||||
readonly page: Page;
|
||||
readonly table: Locator;
|
||||
readonly createUserButton: Locator;
|
||||
readonly searchInput: Locator;
|
||||
readonly searchButton: Locator;
|
||||
readonly successMessage: Locator;
|
||||
readonly pagination: Locator;
|
||||
readonly nextPageButton: Locator;
|
||||
readonly prevPageButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.table = page.locator('.el-table').first();
|
||||
this.createUserButton = 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.pagination = page.locator('.el-pagination').or(page.locator('.pagination'));
|
||||
this.nextPageButton = page.locator('.el-pagination .btn-next').or(page.locator('.pagination .next-page'));
|
||||
this.prevPageButton = page.locator('.el-pagination .btn-prev').or(page.locator('.pagination .prev-page'));
|
||||
}
|
||||
|
||||
async goto() {
|
||||
try {
|
||||
console.log('导航到用户管理页面...');
|
||||
await this.page.goto('/users');
|
||||
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
|
||||
await expect(this.page).toHaveURL(/.*users/);
|
||||
|
||||
console.log('用户管理页面加载完成');
|
||||
} catch (error) {
|
||||
await this.page.screenshot({ path: `test-results/user-management-error-${Date.now()}.png` });
|
||||
|
||||
console.error('导航到用户管理页面失败:', error);
|
||||
|
||||
throw new Error(`导航到用户管理页面失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async waitForTableReady() {
|
||||
await this.table.waitFor({ state: 'visible', timeout: 10000 });
|
||||
|
||||
await this.page.waitForFunction(
|
||||
() => {
|
||||
const rows = document.querySelectorAll('.el-table__body-wrapper tbody tr');
|
||||
return rows.length > 0;
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
).catch(() => {
|
||||
console.log('表格没有数据,继续执行');
|
||||
});
|
||||
}
|
||||
|
||||
async clickCreateUser() {
|
||||
await this.createUserButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async fillUserForm(userData: {
|
||||
username: string;
|
||||
nickname?: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
password: string;
|
||||
confirmPassword?: string;
|
||||
status?: string;
|
||||
}) {
|
||||
const dialog = this.page.locator('.el-dialog');
|
||||
const isCreateMode = !userData.hasOwnProperty('id');
|
||||
|
||||
// 表单字段顺序:
|
||||
// 创建模式:用户名(0), 密码(1), 昵称(2), 邮箱(3), 手机号(4)
|
||||
// 编辑模式:用户名(0), 昵称(1), 邮箱(2), 手机号(3)
|
||||
|
||||
await dialog.locator('input').first().fill(userData.username);
|
||||
|
||||
if (isCreateMode && userData.password) {
|
||||
await dialog.locator('input[type="password"]').fill(userData.password);
|
||||
}
|
||||
|
||||
if (userData.nickname) {
|
||||
const nicknameIndex = isCreateMode ? 2 : 1;
|
||||
await dialog.locator('input').nth(nicknameIndex).fill(userData.nickname);
|
||||
}
|
||||
|
||||
if (userData.email) {
|
||||
const emailIndex = isCreateMode ? 3 : 2;
|
||||
await dialog.locator('input').nth(emailIndex).fill(userData.email);
|
||||
}
|
||||
|
||||
if (userData.phone) {
|
||||
const phoneIndex = isCreateMode ? 4 : 3;
|
||||
await dialog.locator('input').nth(phoneIndex).fill(userData.phone);
|
||||
}
|
||||
|
||||
if (userData.status) {
|
||||
const statusSelect = dialog.locator('.el-form-item').filter({ hasText: '状态' }).locator('.el-select');
|
||||
if (await statusSelect.count() > 0) {
|
||||
await statusSelect.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const statusText = userData.status === '1' || userData.status === 'ACTIVE' ? '正常' : '禁用';
|
||||
const dropdown = this.page.locator('.el-select-dropdown');
|
||||
if (await dropdown.count() > 0) {
|
||||
const options = dropdown.locator('.el-select-dropdown__item');
|
||||
const optionCount = await options.count();
|
||||
|
||||
for (let i = 0; i < optionCount; i++) {
|
||||
const optionText = await options.nth(i).textContent();
|
||||
if (optionText && optionText.includes(statusText)) {
|
||||
await options.nth(i).click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async submitForm() {
|
||||
const dialog = this.page.locator('.el-dialog');
|
||||
const submitButton = dialog.getByRole('button', { name: '确定' }).or(dialog.locator('button:has-text("确定")'));
|
||||
|
||||
await submitButton.click();
|
||||
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
async waitForSuccessMessage(timeout: number = 10000): Promise<boolean> {
|
||||
try {
|
||||
const message = this.page.locator('.el-message--success').or(this.page.locator('.el-message'));
|
||||
await message.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('等待成功消息超时,检查是否有错误消息');
|
||||
|
||||
try {
|
||||
const errorMessage = this.page.locator('.el-message--error').or(this.page.locator('.el-message--warning'));
|
||||
if (await errorMessage.count() > 0) {
|
||||
const errorText = await errorMessage.first().textContent();
|
||||
console.log('发现错误消息:', errorText);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('没有发现错误消息');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async editUser(rowNumber: number) {
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '编辑' }).or(this.page.locator(`tbody tr:nth-child(${rowNumber}) .edit-button`)).click();
|
||||
}
|
||||
|
||||
async deleteUser(rowNumber: number) {
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '删除' }).or(this.page.locator(`tbody tr:nth-child(${rowNumber}) .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 nextPage() {
|
||||
await this.nextPageButton.click();
|
||||
}
|
||||
|
||||
async prevPage() {
|
||||
await this.prevPageButton.click();
|
||||
}
|
||||
|
||||
async getCurrentPage(): Promise<string> {
|
||||
try {
|
||||
const activePage = this.page.locator('.el-pager li.is-active');
|
||||
if (await activePage.count() > 0) {
|
||||
return await activePage.textContent() || '1';
|
||||
}
|
||||
|
||||
const currentPage = this.page.locator('.el-pagination__current');
|
||||
if (await currentPage.count() > 0) {
|
||||
return await currentPage.textContent() || '1';
|
||||
}
|
||||
|
||||
return '1';
|
||||
} catch (error) {
|
||||
console.log('获取当前页码失败,返回默认值');
|
||||
return '1';
|
||||
}
|
||||
}
|
||||
|
||||
async getUserCount(): Promise<number> {
|
||||
return await this.table.locator('tbody tr').count();
|
||||
}
|
||||
|
||||
async getUserName(rowNumber: number): Promise<string | null> {
|
||||
return await this.table.locator(`tbody tr:nth-child(${rowNumber}) td:first-child`).textContent();
|
||||
}
|
||||
|
||||
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 reload() {
|
||||
await this.page.reload();
|
||||
}
|
||||
|
||||
async clickStatusButton(rowNumber: number) {
|
||||
const row = this.table.locator(`tbody tr:nth-child(${rowNumber})`);
|
||||
await row.locator('.el-tag').first().click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const dropdown = this.page.locator('.el-dropdown');
|
||||
if (await dropdown.count() > 0) {
|
||||
const options = dropdown.locator('.el-dropdown-menu__item');
|
||||
const optionCount = await options.count();
|
||||
|
||||
for (let i = 0; i < optionCount; i++) {
|
||||
const optionText = await options.nth(i).textContent();
|
||||
if (optionText && (optionText.includes('启用') || optionText.includes('禁用'))) {
|
||||
await options.nth(i).click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
async clickEditButton(rowNumber: number) {
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '编辑' }).or(this.page.locator(`tbody tr:nth-child(${rowNumber}) .edit-button`)).click();
|
||||
}
|
||||
|
||||
async clickDeleteButton(rowNumber: number) {
|
||||
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '删除' }).or(this.page.locator(`tbody tr:nth-child(${rowNumber}) .delete-button`)).click();
|
||||
}
|
||||
|
||||
async fillNickname(nickname: string) {
|
||||
const dialog = this.page.locator('.el-dialog');
|
||||
await dialog.locator('input').nth(1).fill(nickname);
|
||||
}
|
||||
|
||||
async selectRole(roleName: string) {
|
||||
const dialog = this.page.locator('.el-dialog');
|
||||
const roleSelect = dialog.locator('.el-select');
|
||||
if (await roleSelect.count() > 0) {
|
||||
await roleSelect.first().click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const dropdown = this.page.locator('.el-select-dropdown');
|
||||
if (await dropdown.count() > 0) {
|
||||
const options = dropdown.locator('.el-select-dropdown__item');
|
||||
const optionCount = await options.count();
|
||||
|
||||
for (let i = 0; i < optionCount; i++) {
|
||||
const optionText = await options.nth(i).textContent();
|
||||
if (optionText && optionText.includes(roleName)) {
|
||||
await options.nth(i).click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
|
||||
async clearSearch() {
|
||||
await this.searchInput.fill('');
|
||||
await this.searchButton.click();
|
||||
}
|
||||
|
||||
async getTableRowCount(): Promise<number> {
|
||||
return await this.table.locator('tbody tr').count();
|
||||
}
|
||||
}
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,288 @@
|
||||
export class RetryHelper {
|
||||
static async retry<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
backoff?: boolean;
|
||||
onRetry?: (attempt: number, error: Error) => void;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
delay = 1000,
|
||||
backoff = true,
|
||||
onRetry
|
||||
} = options;
|
||||
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
if (attempt === maxAttempts) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
if (onRetry) {
|
||||
onRetry(attempt, lastError);
|
||||
}
|
||||
|
||||
const currentDelay = backoff ? delay * attempt : delay;
|
||||
await this.sleep(currentDelay);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
}
|
||||
|
||||
static async retryWithCondition<T>(
|
||||
fn: () => Promise<T>,
|
||||
condition: (result: T) => boolean,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
timeout?: number;
|
||||
onRetry?: (attempt: number, lastResult: T) => void;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 10,
|
||||
delay = 500,
|
||||
timeout = 10000,
|
||||
onRetry
|
||||
} = options;
|
||||
|
||||
const startTime = Date.now();
|
||||
let lastResult: T | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
lastResult = await fn();
|
||||
|
||||
if (condition(lastResult)) {
|
||||
return lastResult;
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > timeout) {
|
||||
throw new Error(`Timeout after ${timeout}ms waiting for condition to be met`);
|
||||
}
|
||||
|
||||
if (onRetry && lastResult !== undefined) {
|
||||
onRetry(attempt, lastResult);
|
||||
}
|
||||
|
||||
await this.sleep(delay);
|
||||
} catch (error) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
throw new Error(`Timeout after ${timeout}ms: ${error}`);
|
||||
}
|
||||
|
||||
await this.sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Condition not met after ${maxAttempts} attempts`);
|
||||
}
|
||||
|
||||
static async retryElementAction<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
ignoreErrors?: string[];
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
delay = 1000,
|
||||
ignoreErrors = ['Timeout', 'Element not found', 'Element not visible']
|
||||
} = options;
|
||||
|
||||
return this.retry(fn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: true,
|
||||
onRetry: (attempt, error) => {
|
||||
const shouldIgnore = ignoreErrors.some(ignoredError =>
|
||||
error.message.includes(ignoredError)
|
||||
);
|
||||
|
||||
if (shouldIgnore) {
|
||||
console.log(`Attempt ${attempt} failed with ignorable error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async retryNetworkRequest<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
retryableStatuses?: number[];
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
delay = 2000,
|
||||
retryableStatuses = [408, 429, 500, 502, 503, 504]
|
||||
} = options;
|
||||
|
||||
return this.retry(fn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: true,
|
||||
onRetry: (attempt, error) => {
|
||||
console.log(`Network request attempt ${attempt} failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async retryClick(
|
||||
clickFn: () => Promise<void>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { maxAttempts = 3, delay = 500 } = options;
|
||||
|
||||
return this.retry(clickFn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: false,
|
||||
onRetry: (attempt, error) => {
|
||||
console.log(`Click attempt ${attempt} failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async retryFill(
|
||||
fillFn: () => Promise<void>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { maxAttempts = 3, delay = 500 } = options;
|
||||
|
||||
return this.retry(fillFn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: false,
|
||||
onRetry: (attempt, error) => {
|
||||
console.log(`Fill attempt ${attempt} failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async retryNavigation(
|
||||
navigateFn: () => Promise<void>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { maxAttempts = 3, delay = 1000 } = options;
|
||||
|
||||
return this.retry(navigateFn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: true,
|
||||
onRetry: (attempt, error) => {
|
||||
console.log(`Navigation attempt ${attempt} failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async retryAssertion<T>(
|
||||
assertionFn: () => Promise<T>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const { maxAttempts = 5, delay = 500 } = options;
|
||||
|
||||
return this.retry(assertionFn, {
|
||||
maxAttempts,
|
||||
delay,
|
||||
backoff: false,
|
||||
onRetry: (attempt, error) => {
|
||||
console.log(`Assertion attempt ${attempt} failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
static createRetryPolicy<T>(
|
||||
fn: () => Promise<T>,
|
||||
policy: {
|
||||
maxAttempts: number;
|
||||
initialDelay: number;
|
||||
maxDelay?: number;
|
||||
backoffMultiplier?: number;
|
||||
retryCondition?: (error: Error) => boolean;
|
||||
}
|
||||
): () => Promise<T> {
|
||||
const {
|
||||
maxAttempts,
|
||||
initialDelay,
|
||||
maxDelay = 30000,
|
||||
backoffMultiplier = 2,
|
||||
retryCondition
|
||||
} = policy;
|
||||
|
||||
return async () => {
|
||||
let currentDelay = initialDelay;
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
if (retryCondition && !retryCondition(lastError)) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
if (attempt === maxAttempts) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
console.log(`Attempt ${attempt}/${maxAttempts} failed: ${lastError.message}`);
|
||||
await this.sleep(currentDelay);
|
||||
currentDelay = Math.min(currentDelay * backoffMultiplier, maxDelay);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
}
|
||||
|
||||
static async retryWithTimeout<T>(
|
||||
fn: () => Promise<T>,
|
||||
timeout: number,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
delay?: number;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const { maxAttempts = 3, delay = 1000 } = options;
|
||||
|
||||
return Promise.race([
|
||||
this.retry(fn, { maxAttempts, delay }),
|
||||
new Promise<T>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Operation timed out after ${timeout}ms`)), timeout)
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
export class TestDataCleanup {
|
||||
readonly page: Page;
|
||||
private createdUsers: string[] = [];
|
||||
private createdRoles: string[] = [];
|
||||
private createdMenus: string[] = [];
|
||||
private createdDictTypes: string[] = [];
|
||||
private createdDictData: string[] = [];
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
trackUser(username: string) {
|
||||
this.createdUsers.push(username);
|
||||
}
|
||||
|
||||
trackRole(roleName: string) {
|
||||
this.createdRoles.push(roleName);
|
||||
}
|
||||
|
||||
trackMenu(menuName: string) {
|
||||
this.createdMenus.push(menuName);
|
||||
}
|
||||
|
||||
trackDictType(dictType: string) {
|
||||
this.createdDictTypes.push(dictType);
|
||||
}
|
||||
|
||||
trackDictData(dictData: string) {
|
||||
this.createdDictData.push(dictData);
|
||||
}
|
||||
|
||||
async cleanupAll() {
|
||||
await this.cleanupUsers();
|
||||
await this.cleanupRoles();
|
||||
await this.cleanupMenus();
|
||||
await this.cleanupDictTypes();
|
||||
await this.cleanupDictData();
|
||||
}
|
||||
|
||||
async cleanupUsers() {
|
||||
for (const username of this.createdUsers) {
|
||||
try {
|
||||
await this.deleteUser(username);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete user ${username}:`, error);
|
||||
}
|
||||
}
|
||||
this.createdUsers = [];
|
||||
}
|
||||
|
||||
async cleanupRoles() {
|
||||
for (const roleName of this.createdRoles) {
|
||||
try {
|
||||
await this.deleteRole(roleName);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete role ${roleName}:`, error);
|
||||
}
|
||||
}
|
||||
this.createdRoles = [];
|
||||
}
|
||||
|
||||
async cleanupMenus() {
|
||||
for (const menuName of this.createdMenus) {
|
||||
try {
|
||||
await this.deleteMenu(menuName);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete menu ${menuName}:`, error);
|
||||
}
|
||||
}
|
||||
this.createdMenus = [];
|
||||
}
|
||||
|
||||
async cleanupDictTypes() {
|
||||
for (const dictType of this.createdDictTypes) {
|
||||
try {
|
||||
await this.deleteDictType(dictType);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete dict type ${dictType}:`, error);
|
||||
}
|
||||
}
|
||||
this.createdDictTypes = [];
|
||||
}
|
||||
|
||||
async cleanupDictData() {
|
||||
for (const dictData of this.createdDictData) {
|
||||
try {
|
||||
await this.deleteDictData(dictData);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete dict data ${dictData}:`, error);
|
||||
}
|
||||
}
|
||||
this.createdDictData = [];
|
||||
}
|
||||
|
||||
private async deleteUser(username: string) {
|
||||
try {
|
||||
await this.page.goto('/users');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
const searchInput = this.page.locator('input[placeholder*="搜索"], input[name*="keyword"], .el-input__inner').first();
|
||||
await searchInput.fill(username);
|
||||
|
||||
const searchButton = this.page.getByRole('button', { name: '搜索' }).or(this.page.locator('button:has-text("搜索")'));
|
||||
await searchButton.click();
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
const userRow = this.page.locator('tbody tr').filter({ hasText: username });
|
||||
const rowCount = await userRow.count();
|
||||
|
||||
if (rowCount > 0) {
|
||||
const deleteButton = userRow.locator('.delete-button, .el-button--danger').first();
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-button--primary:has-text("确定")'));
|
||||
await confirmButton.click();
|
||||
await this.page.waitForTimeout(1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete user ${username}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteRole(roleName: string) {
|
||||
try {
|
||||
await this.page.goto('/roles');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
const searchInput = this.page.locator('input[placeholder*="搜索"], input[name*="keyword"], .el-input__inner').first();
|
||||
await searchInput.fill(roleName);
|
||||
|
||||
const searchButton = this.page.getByRole('button', { name: '搜索' }).or(this.page.locator('button:has-text("搜索")'));
|
||||
await searchButton.click();
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
const roleRow = this.page.locator('tbody tr').filter({ hasText: roleName });
|
||||
const rowCount = await roleRow.count();
|
||||
|
||||
if (rowCount > 0) {
|
||||
const deleteButton = roleRow.locator('.delete-button, .el-button--danger').first();
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-button--primary:has-text("确定")'));
|
||||
await confirmButton.click();
|
||||
await this.page.waitForTimeout(1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete role ${roleName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteMenu(menuName: string) {
|
||||
try {
|
||||
await this.page.goto('/menus');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
const menuRow = this.page.locator('tbody tr').filter({ hasText: menuName });
|
||||
const rowCount = await menuRow.count();
|
||||
|
||||
if (rowCount > 0) {
|
||||
const deleteButton = menuRow.locator('.delete-button, .el-button--danger').first();
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-button--primary:has-text("确定")'));
|
||||
await confirmButton.click();
|
||||
await this.page.waitForTimeout(1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete menu ${menuName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteDictType(dictType: string) {
|
||||
try {
|
||||
await this.page.goto('/dict');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
const dictRow = this.page.locator('.dict-type-table tbody tr').filter({ hasText: dictType });
|
||||
const rowCount = await dictRow.count();
|
||||
|
||||
if (rowCount > 0) {
|
||||
const deleteButton = dictRow.locator('.delete-button, .el-button--danger').first();
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-button--primary:has-text("确定")'));
|
||||
await confirmButton.click();
|
||||
await this.page.waitForTimeout(1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete dict type ${dictType}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteDictData(dictData: string) {
|
||||
try {
|
||||
await this.page.goto('/dict');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
const dictRow = this.page.locator('.dict-data-table tbody tr').filter({ hasText: dictData });
|
||||
const rowCount = await dictRow.count();
|
||||
|
||||
if (rowCount > 0) {
|
||||
const deleteButton = dictRow.locator('.delete-button, .el-button--danger').first();
|
||||
await deleteButton.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.el-button--primary:has-text("确定")'));
|
||||
await confirmButton.click();
|
||||
await this.page.waitForTimeout(1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete dict data ${dictData}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
export interface UserData {
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export interface RoleData {
|
||||
roleName: string;
|
||||
roleKey: string;
|
||||
roleSort: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface MenuData {
|
||||
menuName: string;
|
||||
menuType?: string;
|
||||
path?: string;
|
||||
component?: string;
|
||||
permission?: string;
|
||||
sort?: number;
|
||||
visible?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface DictTypeData {
|
||||
dictName: string;
|
||||
dictType: string;
|
||||
status: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface DictDataData {
|
||||
dictLabel: string;
|
||||
dictValue: string;
|
||||
dictType: string;
|
||||
status: string;
|
||||
sort?: number;
|
||||
}
|
||||
|
||||
export class TestDataFactory {
|
||||
static generateTimestamp(): string {
|
||||
return Date.now().toString();
|
||||
}
|
||||
|
||||
static generateRandomString(length: number = 8): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static generateValidEmail(username: string): string {
|
||||
return `${username}@example.com`;
|
||||
}
|
||||
|
||||
static generateValidPhone(): string {
|
||||
const prefix = ['138', '139', '150', '151', '186', '188'];
|
||||
const selectedPrefix = prefix[Math.floor(Math.random() * prefix.length)];
|
||||
const suffix = Math.floor(Math.random() * 100000000).toString().padStart(8, '0');
|
||||
return selectedPrefix + suffix;
|
||||
}
|
||||
|
||||
static generateValidPassword(): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
||||
let password = '';
|
||||
for (let i = 0; i < 12; i++) {
|
||||
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
static createUser(suffix?: string): UserData {
|
||||
const timestamp = this.generateTimestamp();
|
||||
const uniqueSuffix = suffix || this.generateRandomString(4);
|
||||
|
||||
return {
|
||||
username: `testuser_${uniqueSuffix}_${timestamp}`,
|
||||
nickname: `测试用户_${uniqueSuffix}_${timestamp}`,
|
||||
email: this.generateValidEmail(`testuser_${uniqueSuffix}_${timestamp}`),
|
||||
phone: this.generateValidPhone(),
|
||||
password: this.generateValidPassword(),
|
||||
confirmPassword: this.generateValidPassword()
|
||||
};
|
||||
}
|
||||
|
||||
static createAdminUser(): UserData {
|
||||
return {
|
||||
username: 'admin',
|
||||
nickname: '管理员',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138000',
|
||||
password: 'admin123',
|
||||
confirmPassword: 'admin123'
|
||||
};
|
||||
}
|
||||
|
||||
static createRole(suffix?: string): RoleData {
|
||||
const timestamp = this.generateTimestamp();
|
||||
const uniqueSuffix = suffix || this.generateRandomString(4);
|
||||
|
||||
return {
|
||||
roleName: `testrole_${uniqueSuffix}_${timestamp}`,
|
||||
roleKey: `test_role_${uniqueSuffix}_${timestamp}`,
|
||||
roleSort: 1,
|
||||
status: '1'
|
||||
};
|
||||
}
|
||||
|
||||
static createAdminRole(): RoleData {
|
||||
return {
|
||||
roleName: '管理员',
|
||||
roleKey: 'admin',
|
||||
roleSort: 1,
|
||||
status: '1'
|
||||
};
|
||||
}
|
||||
|
||||
static createMenu(suffix?: string, parentId?: string): MenuData {
|
||||
const timestamp = this.generateTimestamp();
|
||||
const uniqueSuffix = suffix || this.generateRandomString(4);
|
||||
|
||||
return {
|
||||
menuName: `测试菜单_${uniqueSuffix}_${timestamp}`,
|
||||
menuType: 'M',
|
||||
path: `/testmenu_${uniqueSuffix}_${timestamp}`,
|
||||
component: `TestMenu${uniqueSuffix}`,
|
||||
permission: `system:testmenu:${uniqueSuffix}:${timestamp}`,
|
||||
sort: 1,
|
||||
visible: '0',
|
||||
status: '0'
|
||||
};
|
||||
}
|
||||
|
||||
static createSubMenu(parentId: string, suffix?: string): MenuData {
|
||||
const menuData = this.createMenu(suffix);
|
||||
menuData.menuType = 'C';
|
||||
menuData.path = `${menuData.path}/submenu`;
|
||||
return menuData;
|
||||
}
|
||||
|
||||
static createDictType(suffix?: string): DictTypeData {
|
||||
const timestamp = this.generateTimestamp();
|
||||
const uniqueSuffix = suffix || this.generateRandomString(4);
|
||||
|
||||
return {
|
||||
dictName: `测试字典类型_${uniqueSuffix}_${timestamp}`,
|
||||
dictType: `test_dict_type_${uniqueSuffix}_${timestamp}`,
|
||||
status: '0',
|
||||
remark: `测试字典类型备注_${uniqueSuffix}_${timestamp}`
|
||||
};
|
||||
}
|
||||
|
||||
static createDictData(dictType: string, suffix?: string): DictDataData {
|
||||
const timestamp = this.generateTimestamp();
|
||||
const uniqueSuffix = suffix || this.generateRandomString(4);
|
||||
|
||||
return {
|
||||
dictLabel: `测试字典数据_${uniqueSuffix}_${timestamp}`,
|
||||
dictValue: `test_dict_value_${uniqueSuffix}_${timestamp}`,
|
||||
dictType: dictType,
|
||||
status: '0',
|
||||
sort: 1
|
||||
};
|
||||
}
|
||||
|
||||
static createBatchUsers(count: number): UserData[] {
|
||||
const users: UserData[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
users.push(this.createUser(`batch_${i}`));
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
static createBatchRoles(count: number): RoleData[] {
|
||||
const roles: RoleData[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
roles.push(this.createRole(`batch_${i}`));
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
static createBatchMenus(count: number): MenuData[] {
|
||||
const menus: MenuData[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
menus.push(this.createMenu(`batch_${i}`));
|
||||
}
|
||||
return menus;
|
||||
}
|
||||
|
||||
static createBatchDictTypes(count: number): DictTypeData[] {
|
||||
const dictTypes: DictTypeData[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
dictTypes.push(this.createDictType(`batch_${i}`));
|
||||
}
|
||||
return dictTypes;
|
||||
}
|
||||
|
||||
static createBatchDictData(dictType: string, count: number): DictDataData[] {
|
||||
const dictData: DictDataData[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
dictData.push(this.createDictData(dictType, `batch_${i}`));
|
||||
}
|
||||
return dictData;
|
||||
}
|
||||
|
||||
static createInvalidUser(): UserData {
|
||||
return {
|
||||
username: '',
|
||||
nickname: '',
|
||||
email: 'invalid-email',
|
||||
phone: 'invalid-phone',
|
||||
password: 'weak',
|
||||
confirmPassword: 'different'
|
||||
};
|
||||
}
|
||||
|
||||
static createInvalidRole(): RoleData {
|
||||
return {
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
roleSort: -1,
|
||||
status: 'invalid'
|
||||
};
|
||||
}
|
||||
|
||||
static createInvalidMenu(): MenuData {
|
||||
return {
|
||||
menuName: '',
|
||||
menuType: 'invalid',
|
||||
path: '',
|
||||
component: '',
|
||||
permission: '',
|
||||
sort: -1,
|
||||
visible: 'invalid',
|
||||
status: 'invalid'
|
||||
};
|
||||
}
|
||||
|
||||
static createLongString(length: number = 1000): string {
|
||||
return this.generateRandomString(length);
|
||||
}
|
||||
|
||||
static createSpecialCharsString(): string {
|
||||
return '!@#$%^&*()_+-=[]{}|;:,.<>?/~`';
|
||||
}
|
||||
|
||||
static createUnicodeString(): string {
|
||||
return '测试中文🎉🚀';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
export class TestHelpers {
|
||||
static async waitForElementVisible(locator: Locator, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForElementHidden(locator: Locator, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'hidden', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async safeClick(locator: Locator, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
await locator.click();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Safe click failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async safeFill(locator: Locator, value: string, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
await locator.clear();
|
||||
await locator.fill(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Safe fill failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async safeSelect(locator: Locator, value: string, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
await locator.selectOption(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Safe select failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async retryOperation<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries: number = 3,
|
||||
delayMs: number = 1000
|
||||
): Promise<T | null> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (attempt === maxRetries) {
|
||||
console.error(`Operation failed after ${maxRetries} attempts:`, error);
|
||||
return null;
|
||||
}
|
||||
console.log(`Attempt ${attempt} failed, retrying in ${delayMs}ms...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static async waitForNetworkIdle(page: Page, timeout: number = 10000): Promise<void> {
|
||||
try {
|
||||
await page.waitForLoadState('networkidle', { timeout });
|
||||
} catch (error) {
|
||||
console.warn('Network idle timeout, continuing...');
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForNavigation(page: Page, urlPattern: RegExp, timeout: number = 10000): Promise<boolean> {
|
||||
try {
|
||||
await page.waitForURL(urlPattern, { timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async handleDialog(page: Page, action: 'accept' | 'dismiss' = 'accept'): Promise<void> {
|
||||
page.on('dialog', async dialog => {
|
||||
if (action === 'accept') {
|
||||
await dialog.accept();
|
||||
} else {
|
||||
await dialog.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async getTableData(table: Locator): Promise<string[][]> {
|
||||
const rows = await table.locator('tbody tr').all();
|
||||
const data: string[][] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const cells = await row.locator('td').allTextContents();
|
||||
data.push(cells);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async findTableRowByContent(table: Locator, content: string): Promise<Locator | null> {
|
||||
const rows = await table.locator('tbody tr').all();
|
||||
|
||||
for (const row of rows) {
|
||||
const textContent = await row.textContent();
|
||||
if (textContent && textContent.includes(content)) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static async scrollToElement(page: Page, locator: Locator): Promise<void> {
|
||||
await locator.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
static async waitForAnimation(locator: Locator): Promise<void> {
|
||||
await locator.waitFor({ state: 'attached' });
|
||||
await locator.evaluate(el => {
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(resolve, 300);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static async takeScreenshot(page: Page, name: string): Promise<void> {
|
||||
await page.screenshot({ path: `test-results/screenshots/${name}.png`, fullPage: true });
|
||||
}
|
||||
|
||||
static async waitForPageLoad(page: Page, timeout: number = 10000): Promise<void> {
|
||||
try {
|
||||
await page.waitForLoadState('load', { timeout });
|
||||
} catch (error) {
|
||||
console.warn('Page load timeout, continuing...');
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForDOMContent(page: Page, timeout: number = 10000): Promise<void> {
|
||||
try {
|
||||
await page.waitForLoadState('domcontentloaded', { timeout });
|
||||
} catch (error) {
|
||||
console.warn('DOM content load timeout, continuing...');
|
||||
}
|
||||
}
|
||||
|
||||
static async isElementVisible(locator: Locator): Promise<boolean> {
|
||||
try {
|
||||
return await locator.isVisible({ timeout: 1000 });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async isElementEnabled(locator: Locator): Promise<boolean> {
|
||||
try {
|
||||
return await locator.isEnabled({ timeout: 1000 });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async getElementText(locator: Locator): Promise<string | null> {
|
||||
try {
|
||||
return await locator.textContent({ timeout: 5000 });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async getElementCount(locator: Locator): Promise<number> {
|
||||
try {
|
||||
return await locator.count();
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForTextContent(locator: Locator, expectedText: string, timeout: number = 5000): Promise<boolean> {
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
const text = await locator.textContent();
|
||||
return text !== null && text.includes(expectedText);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async clearInput(locator: Locator): Promise<void> {
|
||||
await locator.click();
|
||||
await locator.fill('');
|
||||
await locator.press('Control+A');
|
||||
await locator.press('Backspace');
|
||||
}
|
||||
|
||||
static async waitForSuccessMessage(page: Page, timeout: number = 5000): Promise<boolean> {
|
||||
const successMessage = page.locator('.el-message--success, .success-message, [class*="success"]');
|
||||
try {
|
||||
await successMessage.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForErrorMessage(page: Page, timeout: number = 5000): Promise<boolean> {
|
||||
const errorMessage = page.locator('.el-message--error, .error-message, [class*="error"]');
|
||||
try {
|
||||
await errorMessage.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForLoadingComplete(page: Page, timeout: number = 10000): Promise<void> {
|
||||
const loadingSpinner = page.locator('.el-loading-mask, .loading, [class*="loading"]');
|
||||
|
||||
try {
|
||||
await loadingSpinner.waitFor({ state: 'visible', timeout: 2000 });
|
||||
await loadingSpinner.waitFor({ state: 'hidden', timeout });
|
||||
} catch {
|
||||
console.log('No loading spinner found or already hidden');
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForModal(page: Page, timeout: number = 5000): Promise<boolean> {
|
||||
const modal = page.locator('.el-dialog, .modal, [role="dialog"]');
|
||||
try {
|
||||
await modal.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async closeModal(page: Page): Promise<boolean> {
|
||||
const closeButton = page.locator('.el-dialog__close, .modal-close, button[aria-label="Close"]');
|
||||
try {
|
||||
await closeButton.click();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async waitForSelectDropdown(page: Page, timeout: number = 5000): Promise<boolean> {
|
||||
const dropdown = page.locator('.el-select-dropdown, .select-dropdown');
|
||||
try {
|
||||
await dropdown.waitFor({ state: 'visible', timeout });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async selectFromDropdown(page: Page, value: string): Promise<boolean> {
|
||||
const option = page.locator('.el-select-dropdown__item, .select-option').filter({ hasText: value });
|
||||
try {
|
||||
await option.click();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { APIRequestContext } from '@playwright/test';
|
||||
|
||||
export class ApiClient {
|
||||
private request: APIRequestContext;
|
||||
private baseURL: string;
|
||||
|
||||
constructor(request: APIRequestContext, baseURL: string = 'http://localhost:8084') {
|
||||
this.request = request;
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
async login(username: string, password: string): Promise<{ token: string; userId: number }> {
|
||||
const response = await this.request.post(`${this.baseURL}/api/auth/login`, {
|
||||
data: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Login failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
token: data.token,
|
||||
userId: data.userId,
|
||||
};
|
||||
}
|
||||
|
||||
async logout(token: string): Promise<void> {
|
||||
await this.request.post(`${this.baseURL}/api/auth/logout`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getUsers(token: string): Promise<any[]> {
|
||||
const response = await this.request.get(`${this.baseURL}/api/users`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Get users failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async createUser(token: string, userData: any): Promise<any> {
|
||||
const response = await this.request.post(`${this.baseURL}/api/users`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
data: userData,
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Create user failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async updateUser(token: string, userId: number, userData: any): Promise<any> {
|
||||
const response = await this.request.put(`${this.baseURL}/api/users/${userId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
data: userData,
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Update user failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async deleteUser(token: string, userId: number): Promise<void> {
|
||||
const response = await this.request.delete(`${this.baseURL}/api/users/${userId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Delete user failed: ${response.status()}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getRoles(token: string): Promise<any[]> {
|
||||
const response = await this.request.get(`${this.baseURL}/api/roles`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Get roles failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async createRole(token: string, roleData: any): Promise<any> {
|
||||
const response = await this.request.post(`${this.baseURL}/api/roles`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
data: roleData,
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Create role failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async deleteRole(token: string, roleId: number): Promise<void> {
|
||||
const response = await this.request.delete(`${this.baseURL}/api/roles/${roleId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Delete role failed: ${response.status()}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getMenus(token: string): Promise<any[]> {
|
||||
const response = await this.request.get(`${this.baseURL}/api/menus`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Get menus failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<{ status: string }> {
|
||||
const response = await this.request.get(`${this.baseURL}/actuator/health`);
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Health check failed: ${response.status()}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export { TestDataCleanup } from './TestDataCleanup';
|
||||
export { TestDataFactory } from './TestDataFactory';
|
||||
export { RetryHelper } from './RetryHelper';
|
||||
export type {
|
||||
UserData,
|
||||
RoleData,
|
||||
MenuData,
|
||||
DictTypeData,
|
||||
DictDataData
|
||||
} from './TestDataFactory';
|
||||
@@ -0,0 +1,181 @@
|
||||
import { APIRequestContext } from '@playwright/test';
|
||||
|
||||
export interface TestUser {
|
||||
username: string;
|
||||
nickname?: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
password: string;
|
||||
roleIds?: number[];
|
||||
}
|
||||
|
||||
export interface TestRole {
|
||||
roleName: string;
|
||||
roleKey: string;
|
||||
roleSort: string;
|
||||
status: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export class TestDataManager {
|
||||
private static testData: Map<string, any> = new Map();
|
||||
private static apiBaseUrl: string;
|
||||
|
||||
static initialize(apiBaseUrl: string = 'http://localhost:8084') {
|
||||
this.apiBaseUrl = apiBaseUrl;
|
||||
}
|
||||
|
||||
static generateTimestamp(): string {
|
||||
return Date.now().toString();
|
||||
}
|
||||
|
||||
static generateTestUser(override?: Partial<TestUser>): TestUser {
|
||||
const timestamp = this.generateTimestamp();
|
||||
return {
|
||||
username: `testuser_${timestamp}`,
|
||||
nickname: `测试用户${timestamp}`,
|
||||
email: `test_${timestamp}@example.com`,
|
||||
phone: '13800138000',
|
||||
password: 'Test123!@#',
|
||||
roleIds: [],
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
static generateTestRole(override?: Partial<TestRole>): TestRole {
|
||||
const timestamp = this.generateTimestamp();
|
||||
return {
|
||||
roleName: `测试角色_${timestamp}`,
|
||||
roleKey: `test_role_${timestamp}`,
|
||||
roleSort: '1',
|
||||
status: '1',
|
||||
remark: `测试角色备注_${timestamp}`,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
static async createTestUser(request: APIRequestContext, userData: TestUser): Promise<any> {
|
||||
const response = await request.post(`${this.apiBaseUrl}/api/users`, {
|
||||
data: userData,
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to create test user: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const userId = result.data?.id || result.id;
|
||||
|
||||
this.testData.set(`user_${userData.username}`, {
|
||||
id: userId,
|
||||
...userData,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static async createTestRole(request: APIRequestContext, roleData: TestRole): Promise<any> {
|
||||
const response = await request.post(`${this.apiBaseUrl}/api/roles`, {
|
||||
data: roleData,
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to create test role: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const roleId = result.data?.id || result.id;
|
||||
|
||||
this.testData.set(`role_${roleData.roleKey}`, {
|
||||
id: roleId,
|
||||
...roleData,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static async deleteTestUser(request: APIRequestContext, username: string): Promise<void> {
|
||||
const userData = this.testData.get(`user_${username}`);
|
||||
if (!userData || !userData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await request.delete(`${this.apiBaseUrl}/api/users/${userData.id}`);
|
||||
if (!response.ok()) {
|
||||
console.warn(`Failed to delete test user ${username}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
this.testData.delete(`user_${username}`);
|
||||
}
|
||||
|
||||
static async deleteTestRole(request: APIRequestContext, roleKey: string): Promise<void> {
|
||||
const roleData = this.testData.get(`role_${roleKey}`);
|
||||
if (!roleData || !roleData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await request.delete(`${this.apiBaseUrl}/api/roles/${roleData.id}`);
|
||||
if (!response.ok()) {
|
||||
console.warn(`Failed to delete test role ${roleKey}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
this.testData.delete(`role_${roleKey}`);
|
||||
}
|
||||
|
||||
static async cleanupTestData(request: APIRequestContext): Promise<void> {
|
||||
const cleanupPromises: Promise<void>[] = [];
|
||||
|
||||
const entries = Array.from(this.testData.entries());
|
||||
for (const [key, data] of entries) {
|
||||
if (key.startsWith('user_')) {
|
||||
cleanupPromises.push(this.deleteTestUser(request, data.username));
|
||||
} else if (key.startsWith('role_')) {
|
||||
cleanupPromises.push(this.deleteTestRole(request, data.roleKey));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.allSettled(cleanupPromises);
|
||||
this.testData.clear();
|
||||
}
|
||||
|
||||
static getTestData(key: string): any {
|
||||
return this.testData.get(key);
|
||||
}
|
||||
|
||||
static getAllTestData(): Map<string, any> {
|
||||
return new Map(this.testData);
|
||||
}
|
||||
|
||||
static clearTestData(): void {
|
||||
this.testData.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class DatabaseHelper {
|
||||
private static apiBaseUrl: string;
|
||||
|
||||
static initialize(apiBaseUrl: string = 'http://localhost:8084') {
|
||||
this.apiBaseUrl = apiBaseUrl;
|
||||
}
|
||||
|
||||
static async resetDatabase(request: APIRequestContext): Promise<void> {
|
||||
const response = await request.post(`${this.apiBaseUrl}/api/test/reset-database`);
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to reset database: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
static async clearTestData(request: APIRequestContext): Promise<void> {
|
||||
const response = await request.post(`${this.apiBaseUrl}/api/test/clear-test-data`);
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to clear test data: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
static async seedTestData(request: APIRequestContext): Promise<void> {
|
||||
const response = await request.post(`${this.apiBaseUrl}/api/test/seed-test-data`);
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Failed to seed test data: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class TestHelper {
|
||||
static async waitForPageLoad(page: Page, timeout: number = 30000): Promise<void> {
|
||||
await page.waitForLoadState('networkidle', { timeout });
|
||||
await page.waitForLoadState('domcontentloaded', { timeout });
|
||||
}
|
||||
|
||||
static async waitForElementVisible(
|
||||
page: Page,
|
||||
selector: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await expect(page.locator(selector)).toBeVisible({ timeout });
|
||||
}
|
||||
|
||||
static async waitForElementHidden(
|
||||
page: Page,
|
||||
selector: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await expect(page.locator(selector)).toBeHidden({ timeout });
|
||||
}
|
||||
|
||||
static async waitForTextContent(
|
||||
page: Page,
|
||||
selector: string,
|
||||
text: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await expect(page.locator(selector)).toContainText(text, { timeout });
|
||||
}
|
||||
|
||||
static async clickElement(page: Page, selector: string, timeout: number = 10000): Promise<void> {
|
||||
await page.click(selector, { timeout });
|
||||
}
|
||||
|
||||
static async fillInput(
|
||||
page: Page,
|
||||
selector: string,
|
||||
value: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await page.fill(selector, value, { timeout });
|
||||
}
|
||||
|
||||
static async selectOption(
|
||||
page: Page,
|
||||
selector: string,
|
||||
value: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await page.selectOption(selector, value, { timeout });
|
||||
}
|
||||
|
||||
static async checkCheckbox(
|
||||
page: Page,
|
||||
selector: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await page.check(selector, { timeout });
|
||||
}
|
||||
|
||||
static async uncheckCheckbox(
|
||||
page: Page,
|
||||
selector: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await page.uncheck(selector, { timeout });
|
||||
}
|
||||
|
||||
static async uploadFile(
|
||||
page: Page,
|
||||
selector: string,
|
||||
filePath: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
await page.setInputFiles(selector, filePath, { timeout });
|
||||
}
|
||||
|
||||
static async takeScreenshot(
|
||||
page: Page,
|
||||
filename: string,
|
||||
fullPage: boolean = false
|
||||
): Promise<void> {
|
||||
await page.screenshot({
|
||||
path: `test-results/screenshots/${filename}`,
|
||||
fullPage,
|
||||
});
|
||||
}
|
||||
|
||||
static async waitForUrl(
|
||||
page: Page,
|
||||
urlPattern: string | RegExp,
|
||||
timeout: number = 30000
|
||||
): Promise<void> {
|
||||
await page.waitForURL(urlPattern, { timeout });
|
||||
}
|
||||
|
||||
static async reloadPage(page: Page, timeout: number = 30000): Promise<void> {
|
||||
await page.reload({ waitUntil: 'networkidle', timeout });
|
||||
}
|
||||
|
||||
static async navigateTo(page: Page, url: string, timeout: number = 30000): Promise<void> {
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout });
|
||||
}
|
||||
|
||||
static async waitForDialog(page: Page, timeout: number = 10000): Promise<void> {
|
||||
await page.waitForEvent('dialog', { timeout });
|
||||
}
|
||||
|
||||
static async handleDialog(page: Page, accept: boolean = true): Promise<void> {
|
||||
page.on('dialog', async (dialog) => {
|
||||
if (accept) {
|
||||
await dialog.accept();
|
||||
} else {
|
||||
await dialog.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async waitForToast(
|
||||
page: Page,
|
||||
message: string,
|
||||
timeout: number = 5000
|
||||
): Promise<void> {
|
||||
await expect(page.locator('.el-message')).toContainText(message, { timeout });
|
||||
}
|
||||
|
||||
static async waitForSuccessMessage(page: Page, timeout: number = 5000): Promise<void> {
|
||||
await expect(page.locator('.el-message--success')).toBeVisible({ timeout });
|
||||
}
|
||||
|
||||
static async waitForErrorMessage(page: Page, timeout: number = 5000): Promise<void> {
|
||||
await expect(page.locator('.el-message--error')).toBeVisible({ timeout });
|
||||
}
|
||||
|
||||
static async getElementText(page: Page, selector: string): Promise<string> {
|
||||
const text = await page.textContent(selector);
|
||||
return text || '';
|
||||
}
|
||||
|
||||
static async getElementCount(page: Page, selector: string): Promise<number> {
|
||||
return await page.locator(selector).count();
|
||||
}
|
||||
|
||||
static async isElementVisible(page: Page, selector: string): Promise<boolean> {
|
||||
return await page.locator(selector).isVisible();
|
||||
}
|
||||
|
||||
static async isElementEnabled(page: Page, selector: string): Promise<boolean> {
|
||||
return await page.locator(selector).isEnabled();
|
||||
}
|
||||
|
||||
static async scrollToElement(page: Page, selector: string): Promise<void> {
|
||||
await page.locator(selector).scrollIntoViewIfNeeded();
|
||||
}
|
||||
|
||||
static async hoverElement(page: Page, selector: string): Promise<void> {
|
||||
await page.hover(selector);
|
||||
}
|
||||
|
||||
static async doubleClickElement(page: Page, selector: string): Promise<void> {
|
||||
await page.dblclick(selector);
|
||||
}
|
||||
|
||||
static async rightClickElement(page: Page, selector: string): Promise<void> {
|
||||
await page.click(selector, { button: 'right' });
|
||||
}
|
||||
|
||||
static async waitForApiResponse(
|
||||
page: Page,
|
||||
urlPattern: string | RegExp,
|
||||
timeout: number = 30000
|
||||
): Promise<void> {
|
||||
await page.waitForResponse(
|
||||
(response) => !!response.url().match(urlPattern),
|
||||
{ timeout }
|
||||
);
|
||||
}
|
||||
|
||||
static async getApiResponse(
|
||||
page: Page,
|
||||
urlPattern: string | RegExp,
|
||||
timeout: number = 30000
|
||||
): Promise<any> {
|
||||
const response = await page.waitForResponse(
|
||||
(response) => !!response.url().match(urlPattern),
|
||||
{ timeout }
|
||||
);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
static async mockApiResponse(
|
||||
page: Page,
|
||||
urlPattern: string | RegExp,
|
||||
mockData: any
|
||||
): Promise<void> {
|
||||
await page.route(urlPattern, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockData),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static async executeScript(page: Page, script: string): Promise<any> {
|
||||
return await page.evaluate(script);
|
||||
}
|
||||
|
||||
static async setLocalStorage(page: Page, key: string, value: string): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ key, value }) => {
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
{ key, value }
|
||||
);
|
||||
}
|
||||
|
||||
static async getLocalStorage(page: Page, key: string): Promise<string | null> {
|
||||
return await page.evaluate((key) => localStorage.getItem(key), key);
|
||||
}
|
||||
|
||||
static async clearLocalStorage(page: Page): Promise<void> {
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
}
|
||||
|
||||
static async setSessionStorage(page: Page, key: string, value: string): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ key, value }) => {
|
||||
sessionStorage.setItem(key, value);
|
||||
},
|
||||
{ key, value }
|
||||
);
|
||||
}
|
||||
|
||||
static async clearSessionStorage(page: Page): Promise<void> {
|
||||
await page.evaluate(() => sessionStorage.clear());
|
||||
}
|
||||
|
||||
static async clearCookies(page: Page): Promise<void> {
|
||||
await page.context().clearCookies();
|
||||
}
|
||||
|
||||
static async clearAllStorage(page: Page): Promise<void> {
|
||||
await this.clearLocalStorage(page);
|
||||
await this.clearSessionStorage(page);
|
||||
await this.clearCookies(page);
|
||||
}
|
||||
|
||||
static async getAuthToken(page: Page): Promise<string> {
|
||||
const token = await this.getLocalStorage(page, 'token');
|
||||
if (!token) {
|
||||
const user = await this.getLocalStorage(page, 'user');
|
||||
if (user) {
|
||||
const userData = JSON.parse(user);
|
||||
return userData.token || '';
|
||||
}
|
||||
}
|
||||
return token || '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user