feat: 添加测试框架和覆盖率报告功能

feat(测试): 新增Playwright和Vitest测试配置
feat(测试): 添加测试覆盖率报告生成功能
feat(测试): 实现前后端测试脚本集成

fix(测试): 修复测试密码不匹配问题
fix(测试): 修正URL等待策略
fix(测试): 调整错误消息选择器

refactor(测试): 重构测试目录结构
refactor(测试): 优化测试用例组织方式

docs: 更新测试报告文档
docs: 添加测试覆盖率报告模板

ci: 添加Docker测试环境配置
ci: 实现测试自动化脚本

chore: 更新依赖版本
chore: 添加测试相关配置文件
This commit is contained in:
张翔
2026-03-25 09:03:37 +08:00
parent 117978e148
commit e2ad1331cc
126 changed files with 18083 additions and 7805 deletions
@@ -0,0 +1,437 @@
# 测试选择器优化指南
## 概述
本指南提供了在Playwright测试中使用稳定选择器的最佳实践,以提高测试的可靠性和可维护性。
## 选择器优先级
### 1. 推荐的选择器(最稳定)
#### 1.1 使用data-testid属性
```typescript
// ✅ 推荐:使用data-testid
await page.getByTestId('submit-button').click();
await page.getByTestId('username-input').fill('admin');
// ❌ 不推荐:使用CSS类名
await page.click('.btn-primary');
await page.fill('.username-input', 'admin');
```
**在前端添加data-testid**
```vue
<template>
<el-button data-testid="submit-button" type="primary">提交</el-button>
<el-input data-testid="username-input" v-model="username" />
</template>
```
#### 1.2 使用角色和文本
```typescript
// ✅ 推荐:使用角色和文本
await page.getByRole('button', { name: '提交' }).click();
await page.getByRole('textbox', { name: '用户名' }).fill('admin');
// ❌ 不推荐:使用CSS选择器
await page.click('button[type="submit"]');
await page.fill('input[placeholder="用户名"]', 'admin');
```
#### 1.3 使用文本内容
```typescript
// ✅ 推荐:使用文本内容
await page.getByText('登录').click();
await page.getByText('用户管理').click();
// ❌ 不推荐:使用CSS选择器
await page.click('.login-button');
await page.click('.user-management-link');
```
### 2. 可接受的选择器(中等稳定性)
#### 2.1 使用ARIA属性
```typescript
// ✅ 可接受:使用ARIA属性
await page.getByLabel('用户名').fill('admin');
await page.getByPlaceholder('请输入用户名').fill('admin');
await page.getByAltText('Logo').click();
```
#### 2.2 使用表单属性
```typescript
// ✅ 可接受:使用表单属性
await page.getByTitle('提交').click();
await page.getByTestId('username').fill('admin');
```
### 3. 不推荐的选择器(稳定性差)
#### 3.1 避免使用CSS类名
```typescript
// ❌ 不推荐:CSS类名可能变化
await page.click('.el-button--primary');
await page.fill('.el-input__inner', 'admin');
```
#### 3.2 避免使用复杂的CSS选择器
```typescript
// ❌ 不推荐:复杂选择器难以维护
await page.click('.el-form > .el-form-item > .el-form-item__content > .el-button');
await page.fill('div.el-input > div.el-input__wrapper > input', 'admin');
```
#### 3.3 避免使用索引
```typescript
// ❌ 不推荐:索引不稳定
await page.click('button:nth-child(2)');
await page.fill('input:nth-of-type(1)', 'admin');
```
## 选择器优化示例
### 示例1:登录页面
#### 优化前
```typescript
await page.click('.el-button--primary');
await page.fill('.el-input__inner', 'admin');
await page.fill('.el-input__inner', 'admin123');
```
#### 优化后
```typescript
// 在前端添加data-testid
// <el-button data-testid="login-button">登录</el-button>
// <el-input data-testid="username-input" placeholder="用户名" />
// <el-input data-testid="password-input" type="password" placeholder="密码" />
await page.getByTestId('login-button').click();
await page.getByTestId('username-input').fill('admin');
await page.getByTestId('password-input').fill('admin123');
```
### 示例2:用户管理页面
#### 优化前
```typescript
await page.click('.el-table__body tr:first-child .edit-button');
await page.click('.el-dialog__footer button[type="submit"]');
await page.click('.el-message-box__btns .el-button--primary');
```
#### 优化后
```typescript
// 在前端添加data-testid
// <button data-testid="edit-user-button">编辑</button>
// <button data-testid="submit-form-button">提交</button>
// <button data-testid="confirm-delete-button">确认</button>
await page.getByTestId('edit-user-button').click();
await page.getByTestId('submit-form-button').click();
await page.getByTestId('confirm-delete-button').click();
```
### 示例3:表单验证
#### 优化前
```typescript
const errorMessage = await page.textContent('.el-form-item__error');
const hasError = await page.locator('.el-input.is-error').count() > 0;
```
#### 优化后
```typescript
// 在前端添加data-testid
// <div data-testid="username-error" class="el-form-item__error">用户名不能为空</div>
const errorMessage = await page.getByTestId('username-error').textContent();
const hasError = await page.getByTestId('username-error').isVisible();
```
## Page Object优化
### 优化前的Page Object
```typescript
export class UserManagementPage {
readonly page: Page;
readonly table: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.table = page.locator('.el-table');
this.submitButton = page.locator('.el-dialog__footer button[type="submit"]');
}
async clickEditUser(rowNumber: number) {
await this.table.locator(`tbody tr:nth-child(${rowNumber}) .edit-button`).click();
}
async submitForm() {
await this.submitButton.click();
}
}
```
### 优化后的Page Object
```typescript
export class UserManagementPage {
readonly page: Page;
readonly table: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.table = page.getByTestId('user-table');
this.submitButton = page.getByTestId('submit-form-button');
}
async clickEditUser(rowNumber: number) {
await this.table.getByTestId(`edit-user-button-${rowNumber}`).click();
}
async submitForm() {
await this.submitButton.click();
}
async getErrorMessage(fieldName: string): Promise<string> {
return await this.page.getByTestId(`${fieldName}-error`).textContent();
}
}
```
## 前端data-testid添加指南
### 添加原则
1. **关键交互元素**:按钮、链接、输入框
2. **表单元素**:提交按钮、取消按钮、确认按钮
3. **错误消息**:表单验证错误、API错误消息
4. **重要区域**:表格、对话框、侧边栏
### 命名规范
```typescript
// 按钮
data-testid="submit-button"
data-testid="cancel-button"
data-testid="delete-button"
// 输入框
data-testid="username-input"
data-testid="password-input"
data-testid="email-input"
// 表格
data-testid="user-table"
data-testid="role-table"
// 表格行
data-testid="user-row-1"
data-testid="user-row-2"
// 表格操作按钮
data-testid="edit-user-button-1"
data-testid="delete-user-button-1"
// 错误消息
data-testid="username-error"
data-testid="password-error"
data-testid="api-error-message"
// 对话框
data-testid="user-form-dialog"
data-testid="confirm-delete-dialog"
// 菜单
data-testid="user-management-menu"
data-testid="role-management-menu"
```
### Vue组件示例
```vue
<template>
<div class="user-management">
<!-- 表格 -->
<el-table
:data="users"
data-testid="user-table"
>
<el-table-column prop="username" label="用户名" />
<el-table-column label="操作">
<template #default="{ row, $index }">
<el-button
data-testid="edit-user-button-${$index}"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
data-testid="delete-user-button-${$index}"
type="danger"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 创建用户对话框 -->
<el-dialog
v-model="dialogVisible"
data-testid="user-form-dialog"
>
<el-form :model="form" data-testid="user-form">
<el-form-item label="用户名" data-testid="username-form-item">
<el-input
v-model="form.username"
data-testid="username-input"
placeholder="请输入用户名"
/>
<div
v-if="errors.username"
data-testid="username-error"
class="el-form-item__error"
>
{{ errors.username }}
</div>
</el-form-item>
<el-form-item label="密码" data-testid="password-form-item">
<el-input
v-model="form.password"
type="password"
data-testid="password-input"
placeholder="请输入密码"
/>
<div
v-if="errors.password"
data-testid="password-error"
class="el-form-item__error"
>
{{ errors.password }}
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button
data-testid="cancel-button"
@click="dialogVisible = false"
>
取消
</el-button>
<el-button
data-testid="submit-button"
type="primary"
@click="handleSubmit"
>
提交
</el-button>
</template>
</el-dialog>
</div>
</template>
```
## 测试稳定性最佳实践
### 1. 使用稳定的等待策略
```typescript
// ✅ 推荐:使用Playwright内置等待
await page.getByTestId('submit-button').click();
await page.waitForLoadState('networkidle');
// ❌ 不推荐:使用固定等待时间
await page.click('.submit-button');
await page.waitForTimeout(3000);
```
### 2. 使用明确的断言
```typescript
// ✅ 推荐:使用明确的断言
await expect(page.getByTestId('success-message')).toBeVisible();
await expect(page.getByTestId('success-message')).toContainText('操作成功');
// ❌ 不推荐:使用隐式断言
await page.waitForSelector('.success-message');
```
### 3. 使用Page Object模式
```typescript
// ✅ 推荐:使用Page Object
const userPage = new UserManagementPage(page);
await userPage.clickEditUser(1);
await userPage.submitForm();
// ❌ 不推荐:直接操作页面元素
await page.click('.edit-button');
await page.click('.submit-button');
```
### 4. 使用测试辅助工具
```typescript
// ✅ 推荐:使用TestHelper
await TestHelper.waitForElementVisible(page, 'user-form-dialog');
await TestHelper.waitForSuccessMessage(page);
await TestHelper.waitForErrorMessage(page);
// ❌ 不推荐:手动实现等待
await page.waitForSelector('.user-form-dialog');
await page.waitForSelector('.el-message--success');
```
## 迁移计划
### 阶段1:添加data-testid1-2天)
1. 登录页面
2. 用户管理页面
3. 角色管理页面
4. 菜单管理页面
5. 系统配置页面
### 阶段2:更新测试用例(1-2天)
1. 更新LoginPage
2. 更新UserManagementPage
3. 更新RoleManagementPage
4. 更新其他Page Objects
### 阶段3:验证测试稳定性(1天)
1. 运行所有测试
2. 检查测试通过率
3. 修复失败的测试
## 总结
通过使用稳定的选择器策略,我们可以显著提高测试的可靠性和可维护性:
- ✅ 测试更稳定,不易受UI变化影响
- ✅ 测试更易读,意图更明确
- ✅ 测试更易维护,减少选择器更新
- ✅ 测试更可靠,减少偶发性失败
---
**创建时间**2026-03-24
**文档版本**v1.0
@@ -0,0 +1,407 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { TestHelper } from './utils/testHelper';
test.describe('认证异常场景测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
await loginPage.goto();
});
test.afterEach(async ({ page }) => {
await TestHelper.clearAllStorage(page);
});
test('登录失败 - 用户名为空', async ({ page }) => {
await test.step('尝试使用空用户名登录', async () => {
await loginPage.usernameInput.fill('');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
});
await test.step('验证错误提示', async () => {
await TestHelper.waitForElementVisible(page, '.el-form-item__error');
const errorMessage = await TestHelper.getElementText(page, '.el-form-item__error');
expect(errorMessage).toBeTruthy();
});
});
test('登录失败 - 密码为空', async ({ page }) => {
await test.step('尝试使用空密码登录', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('');
await loginPage.loginButton.click();
});
await test.step('验证错误提示', async () => {
await TestHelper.waitForElementVisible(page, '.el-form-item__error');
const errorMessage = await TestHelper.getElementText(page, '.el-form-item__error');
expect(errorMessage).toBeTruthy();
});
});
test('登录失败 - 用户名和密码都为空', async ({ page }) => {
await test.step('尝试使用空用户名和密码登录', async () => {
await loginPage.usernameInput.fill('');
await loginPage.passwordInput.fill('');
await loginPage.loginButton.click();
});
await test.step('验证错误提示', async () => {
const errorMessages = await page.locator('.el-form-item__error').all();
expect(errorMessages.length).toBeGreaterThan(0);
});
});
test('登录失败 - 用户名不存在', async ({ page }) => {
await test.step('尝试使用不存在的用户名登录', async () => {
await loginPage.usernameInput.fill('nonexistentuser123456');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
});
await test.step('验证错误消息', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('用户名或密码错误');
});
});
test('登录失败 - 密码错误', async ({ page }) => {
await test.step('尝试使用错误的密码登录', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('wrongpassword');
await loginPage.loginButton.click();
});
await test.step('验证错误消息', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('用户名或密码错误');
});
});
test('登录失败 - 账户被锁定', async ({ page }) => {
await test.step('连续多次登录失败', async () => {
for (let i = 0; i < 5; i++) {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('wrongpassword');
await loginPage.loginButton.click();
await page.waitForTimeout(1000);
await loginPage.usernameInput.fill('');
await loginPage.passwordInput.fill('');
}
});
await test.step('验证账户锁定提示', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('账户已被锁定');
});
});
test('登录失败 - 账户被禁用', async ({ page, request }) => {
await test.step('禁用admin账户', async () => {
await request.put('http://localhost:8084/api/users/admin/status', {
data: { status: '0' }
});
});
await test.step('尝试使用被禁用的账户登录', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
});
await test.step('验证账户禁用提示', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('账户已被禁用');
});
await test.step('恢复admin账户状态', async () => {
await request.put('http://localhost:8084/api/users/admin/status', {
data: { status: '1' }
});
});
});
test('登录失败 - Token过期', async ({ page }) => {
await test.step('正常登录', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await TestHelper.waitForUrl(page, /.*dashboard/);
});
await test.step('设置过期的Token', async () => {
await TestHelper.setLocalStorage(page, 'token', 'expired_token_123456');
await TestHelper.setLocalStorage(page, 'token_expires', '0');
});
await test.step('刷新页面验证Token过期', async () => {
await page.reload();
await TestHelper.waitForPageLoad(page);
});
await test.step('验证自动跳转到登录页面', async () => {
await TestHelper.waitForUrl(page, /.*login/);
await expect(page).toHaveURL(/.*login/);
});
});
test('登录失败 - 无效的Token格式', async ({ page }) => {
await test.step('设置无效的Token', async () => {
await TestHelper.setLocalStorage(page, 'token', 'invalid_token_format');
});
await test.step('尝试访问需要认证的页面', async () => {
await page.goto('/users');
await TestHelper.waitForPageLoad(page);
});
await test.step('验证自动跳转到登录页面', async () => {
await TestHelper.waitForUrl(page, /.*login/);
await expect(page).toHaveURL(/.*login/);
});
});
test('登出失败 - Token已失效', async ({ page }) => {
await test.step('正常登录', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await TestHelper.waitForUrl(page, /.*dashboard/);
});
await test.step('清除Token', async () => {
await TestHelper.clearLocalStorage(page);
});
await test.step('尝试登出', async () => {
const avatar = page.locator('.el-avatar');
if (await avatar.count() > 0) {
await avatar.click();
await TestHelper.waitForElementVisible(page, '.el-dropdown-menu');
const logoutButton = page.locator('.el-dropdown-menu').getByText('退出登录');
if (await logoutButton.count() > 0) {
await logoutButton.click();
}
}
});
await test.step('验证跳转到登录页面', async () => {
await TestHelper.waitForUrl(page, /.*login/);
await expect(page).toHaveURL(/.*login/);
});
});
test('登录成功 - 记住我功能', async ({ page }) => {
await test.step('启用记住我功能并登录', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
const rememberMeCheckbox = page.locator('.remember-me-checkbox');
if (await rememberMeCheckbox.count() > 0) {
await rememberMeCheckbox.check();
}
await loginPage.loginButton.click();
await TestHelper.waitForUrl(page, /.*dashboard/);
});
await test.step('验证Token持久化', async () => {
const token = await TestHelper.getLocalStorage(page, 'token');
expect(token).toBeTruthy();
const rememberMe = await TestHelper.getLocalStorage(page, 'remember_me');
expect(rememberMe).toBe('true');
});
});
test('登录成功 - 自动填充上次登录用户名', async ({ page }) => {
await test.step('首次登录', async () => {
await loginPage.usernameInput.fill('testuser');
await loginPage.passwordInput.fill('testpassword');
await loginPage.loginButton.click();
await TestHelper.waitForUrl(page, /.*dashboard/);
});
await test.step('登出', async () => {
await loginPage.logout();
await TestHelper.waitForUrl(page, /.*login/);
});
await test.step('验证自动填充上次登录用户名', async () => {
const usernameInput = page.locator('input[placeholder*="用户名"]');
const usernameValue = await usernameInput.inputValue();
expect(usernameValue).toBe('testuser');
});
});
test('登录失败 - SQL注入攻击', async ({ page }) => {
await test.step('尝试SQL注入攻击', async () => {
const sqlInjection = "' OR '1'='1";
await loginPage.usernameInput.fill(sqlInjection);
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
});
await test.step('验证登录失败', async () => {
await TestHelper.waitForErrorMessage(page);
const currentUrl = page.url();
expect(currentUrl).toContain('/login');
});
});
test('登录失败 - XSS攻击', async ({ page }) => {
await test.step('尝试XSS攻击', async () => {
const xssAttack = '<script>alert("XSS")</script>';
await loginPage.usernameInput.fill(xssAttack);
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
});
await test.step('验证XSS被过滤', async () => {
await TestHelper.waitForErrorMessage(page);
const currentUrl = page.url();
expect(currentUrl).toContain('/login');
const usernameInput = page.locator('input[placeholder*="用户名"]');
const usernameValue = await usernameInput.inputValue();
expect(usernameValue).not.toContain('<script>');
});
});
test('登录失败 - 暴力破解防护', async ({ page }) => {
await test.step('快速连续登录失败', async () => {
const loginAttempts = 10;
for (let i = 0; i < loginAttempts; i++) {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill(`wrongpassword${i}`);
await loginPage.loginButton.click();
await page.waitForTimeout(500);
await loginPage.usernameInput.fill('');
await loginPage.passwordInput.fill('');
}
});
await test.step('验证账户被临时锁定', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('登录尝试次数过多');
});
});
test('登录失败 - 网络错误', async ({ page }) => {
await test.step('模拟网络错误', async () => {
await page.route('**/api/auth/login', route => route.abort('failed'));
});
await test.step('尝试登录', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
});
await test.step('验证网络错误提示', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('网络连接失败');
});
});
test('登录失败 - 服务器错误', async ({ page }) => {
await test.step('模拟服务器错误', async () => {
await page.route('**/api/auth/login', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ message: 'Internal Server Error' })
});
});
});
await test.step('尝试登录', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
});
await test.step('验证服务器错误提示', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('服务器错误');
});
});
test('登录成功 - 验证重定向保护', async ({ page }) => {
await test.step('访问受保护页面', async () => {
await page.goto('/users');
await TestHelper.waitForPageLoad(page);
});
await test.step('验证重定向到登录页面', async () => {
await TestHelper.waitForUrl(page, /.*login/);
await expect(page).toHaveURL(/.*login/);
});
await test.step('登录后验证重定向回原页面', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await TestHelper.waitForUrl(page, /.*users/);
await expect(page).toHaveURL(/.*users/);
});
});
test('登录成功 - 验证会话管理', async ({ page, context }) => {
await test.step('正常登录', async () => {
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await TestHelper.waitForUrl(page, /.*dashboard/);
});
await test.step('验证Session Cookie存在', async () => {
const cookies = await context.cookies();
const sessionCookie = cookies.find(c => c.name === 'SESSION' || c.name === 'JSESSIONID');
expect(sessionCookie).toBeDefined();
});
await test.step('验证Token存储在localStorage', async () => {
const token = await TestHelper.getLocalStorage(page, 'token');
expect(token).toBeTruthy();
});
});
test('登录失败 - 验证CSRF保护', async ({ page }) => {
await test.step('检查CSRF Token', async () => {
const csrfToken = page.locator('input[name="csrf_token"]');
const hasCsrfToken = await csrfToken.count() > 0;
if (hasCsrfToken) {
const csrfValue = await csrfToken.inputValue();
expect(csrfValue).toBeTruthy();
expect(csrfValue.length).toBeGreaterThan(10);
}
});
});
});
+410
View File
@@ -0,0 +1,410 @@
import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
import * as fs from 'fs';
import * as path from 'path';
class CustomReporter implements Reporter {
private results: Map<string, TestCase[]> = new Map();
private suiteResults: Map<string, Suite> = new Map();
private startTime: number = Date.now();
private testResults: TestResult[] = [];
onBegin(config: FullConfig) {
console.log(`🚀 开始测试执行: ${config.projects.map(p => p.name).join(', ')}`);
this.startTime = Date.now();
}
onTestBegin(test: TestCase, result: TestResult) {
console.log(`📝 开始测试: ${test.title}`);
}
onTestEnd(test: TestCase, result: TestResult) {
console.log(`✅ 测试完成: ${test.title} - ${result.status}`);
this.testResults.push(result);
}
onEnd(result: FullResult) {
const endTime = Date.now();
const duration = endTime - this.startTime;
console.log(`🎉 测试执行完成`);
console.log(`⏱️ 总耗时: ${this.formatDuration(duration)}`);
const stats = this.calculateStats(result);
this.generateConsoleReport(stats);
this.generateHtmlReport(result, stats);
this.generateJsonReport(result, stats);
}
private calculateStats(result: FullResult): TestStats {
const suites = result.suites || [];
const allTests = suites.flatMap(suite =>
suite.specs.flatMap(spec => spec.tests)
);
const passed = allTests.filter(t => t.status === 'passed');
const failed = allTests.filter(t => t.status === 'failed');
const skipped = allTests.filter(t => t.status === 'skipped');
const flaky = allTests.filter(t => t.status === 'passed' && t.retry >= 1);
const totalDuration = allTests.reduce((sum, t) => sum + (t.duration || 0), 0);
const avgDuration = totalDuration / allTests.length;
const passRate = (passed.length / allTests.length) * 100;
const failRate = (failed.length / allTests.length) * 100;
const skipRate = (skipped.length / allTests.length) * 100;
const flakyRate = (flaky.length / allTests.length) * 100;
return {
total: allTests.length,
passed: passed.length,
failed: failed.length,
skipped: skipped.length,
flaky: flaky.length,
passRate,
failRate,
skipRate,
flakyRate,
totalDuration,
avgDuration,
slowestTests: allTests
.filter(t => t.duration)
.sort((a, b) => (b.duration || 0) - (a.duration || 0))
.slice(0, 10),
failedTests: failed,
};
}
private generateConsoleReport(stats: TestStats) {
console.log('');
console.log('═══════════════════════════════════════════');
console.log('📊 测试统计报告');
console.log('═══════════════════════════════════════════');
console.log('');
console.log(`📈 总测试数: ${stats.total}`);
console.log(`✅ 通过: ${stats.passed} (${stats.passRate.toFixed(2)}%)`);
console.log(`❌ 失败: ${stats.failed} (${stats.failRate.toFixed(2)}%)`);
console.log(`⏭️ 跳过: ${stats.skipped} (${stats.skipRate.toFixed(2)}%)`);
console.log(`🔄 不稳定: ${stats.flaky} (${stats.flakyRate.toFixed(2)}%)`);
console.log('');
console.log(`⏱️ 总耗时: ${this.formatDuration(stats.totalDuration)}`);
console.log(`⏱️ 平均耗时: ${this.formatDuration(stats.avgDuration)}`);
console.log('');
console.log('🐌 最慢的10个测试:');
stats.slowestTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.title} - ${this.formatDuration(test.duration || 0)}`);
});
console.log('');
if (stats.failedTests.length > 0) {
console.log('❌ 失败的测试:');
stats.failedTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.title}`);
console.log(` 位置: ${test.location.file}:${test.location.line}`);
console.log(` 错误: ${test.error?.message}`);
});
console.log('');
}
}
private generateHtmlReport(result: FullResult, stats: TestStats) {
const html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试报告 - Novalon管理系统</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
padding: 30px;
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.header h1 {
margin: 0;
color: #333;
font-size: 28px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.stat-card h3 {
margin: 0 0 10px 0;
font-size: 14px;
opacity: 0.9;
}
.stat-card .value {
font-size: 32px;
font-weight: bold;
margin: 0;
}
.stat-card .label {
font-size: 12px;
opacity: 0.8;
margin-top: 5px;
}
.stat-card.passed {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.stat-card.failed {
background: linear-gradient(135deg, #ef4444 0%, #f44336 100%);
}
.stat-card.flaky {
background: linear-gradient(135deg, #f59e0b 0%, #f093fb 100%);
}
.section {
margin-bottom: 30px;
}
.section h2 {
color: #333;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
margin-bottom: 20px;
}
.test-list {
list-style: none;
padding: 0;
}
.test-item {
padding: 15px;
margin-bottom: 10px;
border-left: 4px solid #ddd;
background: #f9f9f9;
border-radius: 5px;
}
.test-item.passed {
border-left-color: #38ef7d;
background: #f0fff4;
}
.test-item.failed {
border-left-color: #ef4444;
background: #fff5f5;
}
.test-item.skipped {
border-left-color: #f59e0b;
background: #fef9c3;
}
.test-item.flaky {
border-left-color: #f093fb;
background: #fef3c7;
}
.test-item .test-name {
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.test-item .test-duration {
color: #666;
font-size: 12px;
}
.test-item .test-error {
color: #ef4444;
font-size: 12px;
margin-top: 5px;
padding: 10px;
background: #fee;
border-radius: 3px;
}
.progress-bar {
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin: 20px 0;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
transition: width 0.5s ease;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
color: #666;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 Novalon管理系统测试报告</h1>
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
</div>
<div class="stats-grid">
<div class="stat-card passed">
<h3>通过测试</h3>
<div class="value">${stats.passed}</div>
<div class="label">${stats.passRate.toFixed(2)}%</div>
</div>
<div class="stat-card failed">
<h3>失败测试</h3>
<div class="value">${stats.failed}</div>
<div class="label">${stats.failRate.toFixed(2)}%</div>
</div>
<div class="stat-card flaky">
<h3>不稳定测试</h3>
<div class="value">${stats.flaky}</div>
<div class="label">${stats.flakyRate.toFixed(2)}%</div>
</div>
<div class="stat-card">
<h3>总测试数</h3>
<div class="value">${stats.total}</div>
<div class="label">100%</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-bar-fill" style="width: ${stats.passRate}%"></div>
</div>
<div class="section">
<h2>📈 测试统计</h2>
<ul class="test-list">
<li class="test-item">
<div class="test-name">总耗时</div>
<div class="test-duration">${this.formatDuration(stats.totalDuration)}</div>
</li>
<li class="test-item">
<div class="test-name">平均耗时</div>
<div class="test-duration">${this.formatDuration(stats.avgDuration)}</div>
</li>
<li class="test-item">
<div class="test-name">跳过测试</div>
<div class="test-duration">${stats.skipped} (${stats.skipRate.toFixed(2)}%)</div>
</li>
</ul>
</div>
${stats.failedTests.length > 0 ? `
<div class="section">
<h2>❌ 失败测试详情</h2>
<ul class="test-list">
${stats.failedTests.map(test => `
<li class="test-item failed">
<div class="test-name">${test.title}</div>
<div class="test-duration">${this.formatDuration(test.duration || 0)}</div>
<div class="test-error">
<strong>错误:</strong> ${test.error?.message || '未知错误'}
</div>
</li>
`).join('')}
</ul>
</div>
` : ''}
<div class="section">
<h2>🐌 最慢的10个测试</h2>
<ul class="test-list">
${stats.slowestTests.map((test, index) => `
<li class="test-item ${test.status}">
<div class="test-name">${index + 1}. ${test.title}</div>
<div class="test-duration">${this.formatDuration(test.duration || 0)}</div>
</li>
`).join('')}
</ul>
</div>
<div class="footer">
<p>🧪 Novalon管理系统 - 自动化测试报告</p>
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
</div>
</div>
</body>
</html>
`;
const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.html');
fs.writeFileSync(reportPath, html, 'utf-8');
console.log(`📄 HTML报告已生成: ${reportPath}`);
}
private generateJsonReport(result: FullResult, stats: TestStats) {
const report = {
summary: {
timestamp: new Date().toISOString(),
total: stats.total,
passed: stats.passed,
failed: stats.failed,
skipped: stats.skipped,
flaky: stats.flaky,
passRate: stats.passRate,
failRate: stats.failRate,
skipRate: stats.skipRate,
flakyRate: stats.flakyRate,
totalDuration: stats.totalDuration,
avgDuration: stats.avgDuration,
},
failedTests: stats.failedTests.map(test => ({
title: test.title,
location: test.location,
error: test.error?.message,
duration: test.duration,
})),
slowestTests: stats.slowestTests.map(test => ({
title: test.title,
duration: test.duration,
})),
};
const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
console.log(`📄 JSON报告已生成: ${reportPath}`);
}
private formatDuration(ms: number): string {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
return `${(ms / 60000).toFixed(1)}m`;
}
}
}
interface TestStats {
total: number;
passed: number;
failed: number;
skipped: number;
flaky: number;
passRate: number;
failRate: number;
skipRate: number;
flakyRate: number;
totalDuration: number;
avgDuration: number;
slowestTests: TestCase[];
}
export default CustomReporter;
@@ -0,0 +1,323 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';
import { RoleManagementPage } from './pages/RoleManagementPage';
import { TestHelper } from './utils/testHelper';
test.describe('边缘场景测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let userManagementPage: UserManagementPage;
let roleManagementPage: RoleManagementPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
userManagementPage = new UserManagementPage(page);
roleManagementPage = new RoleManagementPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
});
test.afterEach(async ({ page }) => {
await TestHelper.clearAllStorage(page);
});
test.describe('边界值测试', () => {
test('用户名边界值 - 最小长度', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建最小长度用户名的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const minUsername = 'ab';
await userManagementPage.fillUserForm({
username: minUsername,
email: 'test@example.com',
password: 'password123'
});
await userManagementPage.submitForm();
});
await test.step('验证用户创建成功', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('用户名边界值 - 最大长度', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建最大长度用户名的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const maxUsername = 'a'.repeat(50);
await userManagementPage.fillUserForm({
username: maxUsername,
email: 'test@example.com',
password: 'password123'
});
await userManagementPage.submitForm();
});
await test.step('验证用户创建成功', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('密码边界值 - 最小长度', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建最小长度密码的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const minPassword = 'a'.repeat(6);
await userManagementPage.fillUserForm({
username: 'testuser',
email: 'test@example.com',
password: minPassword
});
await userManagementPage.submitForm();
});
await test.step('验证用户创建成功', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('密码边界值 - 最大长度', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建最大长度密码的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const maxPassword = 'a'.repeat(20);
await userManagementPage.fillUserForm({
username: 'testuser',
email: 'test@example.com',
password: maxPassword
});
await userManagementPage.submitForm();
});
await test.step('验证用户创建成功', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
});
test.describe('空值和null值测试', () => {
test('用户创建 - 用户名为空', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建用户名为空的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUserForm({
username: '',
email: 'test@example.com',
password: 'password123'
});
await userManagementPage.submitForm();
});
await test.step('验证用户名必填验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('用户名不能为空');
});
});
test('用户创建 - 密码为空', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建密码为空的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUserForm({
username: 'testuser',
email: 'test@example.com',
password: ''
});
await userManagementPage.submitForm();
});
await test.step('验证密码必填验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('密码不能为空');
});
});
test('用户创建 - 邮箱为空', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建邮箱为空的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUserForm({
username: 'testuser',
email: '',
password: 'password123'
});
await userManagementPage.submitForm();
});
await test.step('验证邮箱必填验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('邮箱不能为空');
});
});
});
test.describe('特殊字符和格式测试', () => {
test('用户名 - 包含中文字符', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建包含中文的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUserForm({
username: '测试用户',
email: 'test@example.com',
password: 'password123'
});
await userManagementPage.submitForm();
});
await test.step('验证中文用户名处理', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('用户名 - 包含emoji表情', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建包含emoji的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUserForm({
username: 'test😀user',
email: 'test@example.com',
password: 'password123'
});
await userManagementPage.submitForm();
});
await test.step('验证emoji用户名处理', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('密码 - 包含特殊字符', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建包含特殊字符密码的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUserForm({
username: 'testuser',
email: 'test@example.com',
password: 'P@ssw0rd!#$'
});
await userManagementPage.submitForm();
});
await test.step('验证特殊字符密码处理', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
});
test.describe('并发和竞态条件测试', () => {
test('快速连续操作', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('快速连续点击创建按钮', async () => {
for (let i = 0; i < 3; i++) {
await page.click('.create-button');
await page.waitForTimeout(100);
}
});
await test.step('验证重复点击处理', async () => {
const dialogs = await page.locator('.el-dialog').count();
expect(dialogs).toBe(1);
});
});
});
test.describe('国际化场景测试', () => {
test('中文界面操作', async ({ page }) => {
await test.step('验证中文界面显示', async () => {
const dashboardTitle = await page.textContent('h1');
expect(dashboardTitle).toContain('仪表盘');
});
await test.step('验证中文按钮文本', async () => {
const createButton = await page.textContent('.create-button');
expect(createButton).toContain('创建');
});
});
test('中英文混合输入', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建中英文混合用户名的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUserForm({
username: 'test测试user',
email: 'test@example.com',
password: 'password123'
});
await userManagementPage.submitForm();
});
await test.step('验证中英文混合处理', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
});
});
+534
View File
@@ -0,0 +1,534 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';
import { RoleManagementPage } from './pages/RoleManagementPage';
import { TestHelper } from './utils/testHelper';
test.describe('边缘场景测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let userManagementPage: UserManagementPage;
let roleManagementPage: RoleManagementPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
userManagementPage = new UserManagementPage(page);
roleManagementPage = new RoleManagementPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
});
test.afterEach(async ({ page }) => {
await TestHelper.clearAllStorage(page);
});
test.describe('边界值测试', () => {
test('用户名边界值 - 最小长度', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建最小长度用户名的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const minUsername = 'ab';
await userManagementPage.fillUserForm({
username: minUsername,
email: 'test@example.com',
password: 'password123'
});
await userManagementPage.submitForm();
});
await test.step('验证用户创建成功', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('用户名边界值 - 最大长度', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建最大长度用户名的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const maxUsername = 'a'.repeat(50);
await userManagementPage.fillUsername(maxUsername);
await userManagementPage.fillPassword('password123');
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证用户创建成功', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('用户名边界值 - 超过最大长度', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建超过最大长度用户名的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const exceedUsername = 'a'.repeat(51);
await userManagementPage.fillUsername(exceedUsername);
await userManagementPage.fillPassword('password123');
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证用户名长度验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('用户名长度不能超过50个字符');
});
});
test('密码边界值 - 最小长度', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建最小长度密码的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('testuser');
const minPassword = 'a'.repeat(6);
await userManagementPage.fillPassword(minPassword);
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证用户创建成功', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('密码边界值 - 最大长度', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建最大长度密码的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('testuser');
const maxPassword = 'a'.repeat(20);
await userManagementPage.fillPassword(maxPassword);
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证用户创建成功', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('密码边界值 - 低于最小长度', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建低于最小长度密码的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('testuser');
const shortPassword = 'a'.repeat(5);
await userManagementPage.fillPassword(shortPassword);
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证密码长度验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('密码长度不能少于6个字符');
});
});
test('邮箱边界值 - 无效格式', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建无效邮箱格式的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('testuser');
await userManagementPage.fillPassword('password123');
await userManagementPage.fillEmail('invalid-email');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证邮箱格式验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('邮箱格式不正确');
});
});
test('角色名边界值 - 特殊字符', async ({ page }) => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建包含特殊字符的角色', async () => {
await roleManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const specialCharRole = '角色@#$%';
await roleManagementPage.fillRoleName(specialCharRole);
await roleManagementPage.fillRoleKey('ROLE_SPECIAL');
await roleManagementPage.clickSaveButton();
});
await test.step('验证特殊字符处理', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
});
test.describe('空值和null值测试', () => {
test('用户创建 - 用户名为空', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建用户名为空的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('');
await userManagementPage.fillPassword('password123');
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证用户名必填验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('用户名不能为空');
});
});
test('用户创建 - 密码为空', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建密码为空的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('testuser');
await userManagementPage.fillPassword('');
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证密码必填验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('密码不能为空');
});
});
test('用户创建 - 邮箱为空', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建邮箱为空的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('testuser');
await userManagementPage.fillPassword('password123');
await userManagementPage.fillEmail('');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证邮箱必填验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('邮箱不能为空');
});
});
test('用户创建 - 角色为空', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建角色为空的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('testuser');
await userManagementPage.fillPassword('password123');
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.clickSaveButton();
});
await test.step('验证角色必填验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('角色不能为空');
});
});
test('角色创建 - 角色名为空', async ({ page }) => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建角色名为空的角色', async () => {
await roleManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await roleManagementPage.fillRoleName('');
await roleManagementPage.fillRoleKey('ROLE_EMPTY');
await roleManagementPage.clickSaveButton();
});
await test.step('验证角色名必填验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('角色名不能为空');
});
});
test('角色创建 - 角色键为空', async ({ page }) => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建角色键为空的角色', async () => {
await roleManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await roleManagementPage.fillRoleName('测试角色');
await roleManagementPage.fillRoleKey('');
await roleManagementPage.clickSaveButton();
});
await test.step('验证角色键必填验证', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('角色键不能为空');
});
});
});
test.describe('特殊字符和格式测试', () => {
test('用户名 - 包含中文字符', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建包含中文的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('测试用户');
await userManagementPage.fillPassword('password123');
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证中文用户名处理', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('用户名 - 包含emoji表情', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建包含emoji的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('test😀user');
await userManagementPage.fillPassword('password123');
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证emoji用户名处理', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('密码 - 包含特殊字符', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建包含特殊字符密码的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('testuser');
await userManagementPage.fillPassword('P@ssw0rd!#$');
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证特殊字符密码处理', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
test('邮箱 - 包含特殊字符', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建包含特殊字符邮箱的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('testuser');
await userManagementPage.fillPassword('password123');
await userManagementPage.fillEmail('test.user+tag@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证特殊字符邮箱处理', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
});
test.describe('并发和竞态条件测试', () => {
test('并发创建相同用户名', async ({ page, context }) => {
const page1 = page;
const page2 = await context.newPage();
await test.step('在两个页面同时创建相同用户名的用户', async () => {
await page1.goto('/users');
await page2.goto('/users');
await TestHelper.waitForPageLoad(page1);
await TestHelper.waitForPageLoad(page2);
await page1.click('.create-button');
await page2.click('.create-button');
await TestHelper.waitForElementVisible(page1, '.el-dialog');
await TestHelper.waitForElementVisible(page2, '.el-dialog');
await page1.fill('input[name="username"]', 'duplicateuser');
await page2.fill('input[name="username"]', 'duplicateuser');
await page1.fill('input[name="password"]', 'password123');
await page2.fill('input[name="password"]', 'password123');
await page1.fill('input[name="email"]', 'test1@example.com');
await page2.fill('input[name="email"]', 'test2@example.com');
await page1.click('.el-dialog__footer button[type="submit"]');
await page2.click('.el-dialog__footer button[type="submit"]');
});
await test.step('验证并发冲突处理', async () => {
await TestHelper.waitForPageLoad(page1);
await TestHelper.waitForPageLoad(page2);
const errorMessage1 = await TestHelper.getElementText(page1, '.el-message__content');
const errorMessage2 = await TestHelper.getElementText(page2, '.el-message__content');
expect(errorMessage1 || errorMessage2).toContain('用户名已存在');
});
});
test('快速连续操作', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('快速连续点击创建按钮', async () => {
for (let i = 0; i < 3; i++) {
await page.click('.create-button');
await page.waitForTimeout(100);
}
});
await test.step('验证重复点击处理', async () => {
const dialogs = await page.locator('.el-dialog').count();
expect(dialogs).toBe(1);
});
});
});
test.describe('国际化场景测试', () => {
test('中文界面操作', async ({ page }) => {
await test.step('验证中文界面显示', async () => {
const dashboardTitle = await page.textContent('h1');
expect(dashboardTitle).toContain('仪表盘');
});
await test.step('验证中文按钮文本', async () => {
const createButton = await page.textContent('.create-button');
expect(createButton).toContain('创建');
});
await test.step('验证中文表单标签', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
const usernameLabel = await page.textContent('label[for="username"]');
expect(usernameLabel).toContain('用户名');
});
});
test('中英文混合输入', async ({ page }) => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
await test.step('创建中英文混合用户名的用户', async () => {
await userManagementPage.clickCreateButton();
await TestHelper.waitForElementVisible(page, '.el-dialog');
await userManagementPage.fillUsername('test测试user');
await userManagementPage.fillPassword('password123');
await userManagementPage.fillEmail('test@example.com');
await userManagementPage.selectRole('管理员');
await userManagementPage.clickSaveButton();
});
await test.step('验证中英文混合处理', async () => {
await TestHelper.waitForSuccessMessage(page);
const successMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(successMessage).toContain('创建成功');
});
});
});
});
@@ -64,11 +64,11 @@ export class RoleManagementPage {
}
async confirmDelete() {
await this.page.getByRole('button', { name: '确定' }).or(page.locator('.confirm-dialog .confirm-button')).click();
await this.page.getByRole('button', { name: '确定' }).or(this.page.locator('.confirm-dialog .confirm-button')).click();
}
async openPermissionDialog(rowNumber: number) {
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '权限' }).or(page.locator(`tbody tr:nth-child(${rowNumber}) .permission-button`)).click();
await this.table.locator(`tbody tr:nth-child(${rowNumber})`).getByRole('button', { name: '权限' }).or(this.page.locator(`tbody tr:nth-child(${rowNumber}) .permission-button`)).click();
}
async selectPermission(permissionValue: string) {
@@ -0,0 +1,312 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';
import { RoleManagementPage } from './pages/RoleManagementPage';
import { TestHelper } from './utils/testHelper';
test.describe('测试并行化验证', () => {
test('并行执行多个独立测试', async ({ page, context }) => {
const page1 = page;
const page2 = await context.newPage();
const page3 = await context.newPage();
const loginPage1 = new LoginPage(page1);
const loginPage2 = new LoginPage(page2);
const loginPage3 = new LoginPage(page3);
const startTime = Date.now();
await test.step('并行登录三个页面', async () => {
await Promise.all([
loginPage1.goto(),
loginPage2.goto(),
loginPage3.goto()
]);
await Promise.all([
loginPage1.login('admin', 'admin123'),
loginPage2.login('admin', 'admin123'),
loginPage3.login('admin', 'admin123')
]);
});
const endTime = Date.now();
const parallelTime = endTime - startTime;
console.log(`并行登录时间: ${parallelTime}ms`);
expect(parallelTime).toBeLessThan(5000);
await test.step('验证所有页面登录成功', async () => {
const url1 = page1.url();
const url2 = page2.url();
const url3 = page3.url();
expect(url1).toContain('/dashboard');
expect(url2).toContain('/dashboard');
expect(url3).toContain('/dashboard');
});
});
test('并行加载不同模块', async ({ page, context }) => {
const page1 = page;
const page2 = await context.newPage();
const page3 = await context.newPage();
const loginPage1 = new LoginPage(page1);
const loginPage2 = new LoginPage(page2);
const loginPage3 = new LoginPage(page3);
await loginPage1.goto();
await loginPage1.login('admin', 'admin123');
await loginPage2.goto();
await loginPage2.login('admin', 'admin123');
await loginPage3.goto();
await loginPage3.login('admin', 'admin123');
const startTime = Date.now();
await test.step('并行加载用户、角色、设置模块', async () => {
await Promise.all([
page1.goto('/users'),
page2.goto('/roles'),
page3.goto('/settings')
]);
await Promise.all([
page1.waitForSelector('[data-testid="user-table"]'),
page2.waitForSelector('[data-testid="role-table"]'),
page3.waitForSelector('[data-testid="settings-form"]')
]);
});
const endTime = Date.now();
const parallelLoadTime = endTime - startTime;
console.log(`并行加载时间: ${parallelLoadTime}ms`);
expect(parallelLoadTime).toBeLessThan(3000);
});
test('并发API请求性能', async ({ page, request }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await test.step('并发发送多个API请求', async () => {
const token = await TestHelper.getAuthToken(page);
const promises = [
request.get('http://localhost:8084/api/users', {
headers: { 'Authorization': `Bearer ${token}` }
}),
request.get('http://localhost:8084/api/roles', {
headers: { 'Authorization': `Bearer ${token}` }
}),
request.get('http://localhost:8084/api/permissions', {
headers: { 'Authorization': `Bearer ${token}` }
}),
request.get('http://localhost:8084/api/departments', {
headers: { 'Authorization': `Bearer ${token}` }
})
];
const results = await Promise.all(promises);
expect(results[0].status()).toBe(200);
expect(results[1].status()).toBe(200);
expect(results[2].status()).toBe(200);
expect(results[3].status()).toBe(200);
});
const endTime = Date.now();
const concurrentApiTime = endTime - startTime;
console.log(`并发API请求时间: ${concurrentApiTime}ms`);
expect(concurrentApiTime).toBeLessThan(2000);
});
test('测试隔离验证', async ({ page, context }) => {
const page1 = page;
const page2 = await context.newPage();
const loginPage1 = new LoginPage(page1);
const loginPage2 = new LoginPage(page2);
await loginPage1.goto();
await loginPage1.login('admin', 'admin123');
await loginPage2.goto();
await loginPage2.login('testuser', 'test123');
await test.step('验证页面状态隔离', async () => {
const url1 = page1.url();
const url2 = page2.url();
expect(url1).toContain('/dashboard');
expect(url2).toContain('/dashboard');
const storage1 = await page1.evaluate(() => {
return localStorage.getItem('user');
});
const storage2 = await page2.evaluate(() => {
return localStorage.getItem('user');
});
expect(storage1).not.toBe(storage2);
});
await test.step('验证页面操作隔离', async () => {
await page1.goto('/users');
await page2.goto('/roles');
await page1.waitForSelector('[data-testid="user-table"]');
await page2.waitForSelector('[data-testid="role-table"]');
const url1 = page1.url();
const url2 = page2.url();
expect(url1).toContain('/users');
expect(url2).toContain('/roles');
});
});
});
test.describe('测试分组策略', () => {
test('按模块分组执行', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
const userModuleTests = [
async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
},
async () => {
await page.goto('/users/create');
await page.waitForSelector('[data-testid="user-form"]');
}
];
const roleModuleTests = [
async () => {
await page.goto('/roles');
await page.waitForSelector('[data-testid="role-table"]');
},
async () => {
await page.goto('/roles/create');
await page.waitForSelector('[data-testid="role-form"]');
}
];
const startTime = Date.now();
await test.step('按模块顺序执行测试', async () => {
for (const test of userModuleTests) {
await test();
}
for (const test of roleModuleTests) {
await test();
}
});
const endTime = Date.now();
const sequentialTime = endTime - startTime;
console.log(`顺序执行时间: ${sequentialTime}ms`);
});
test('按优先级分组执行', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
const highPriorityTests = [
async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
}
];
const lowPriorityTests = [
async () => {
await page.goto('/settings');
await page.waitForSelector('[data-testid="settings-form"]');
}
];
const startTime = Date.now();
await test.step('按优先级执行测试', async () => {
for (const test of highPriorityTests) {
await test();
}
for (const test of lowPriorityTests) {
await test();
}
});
const endTime = Date.now();
const priorityTime = endTime - startTime;
console.log(`优先级执行时间: ${priorityTime}ms`);
});
});
test.describe('测试依赖优化', () => {
test('减少测试间依赖', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
const startTime = Date.now();
await test.step('执行独立测试', async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
await page.goto('/roles');
await page.waitForSelector('[data-testid="role-table"]');
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
});
const endTime = Date.now();
const independentTime = endTime - startTime;
console.log(`独立测试执行时间: ${independentTime}ms`);
expect(independentTime).toBeLessThan(5000);
});
test('优化测试清理逻辑', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
const startTime = Date.now();
await test.step('快速清理测试状态', async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
await TestHelper.clearAllStorage(page);
await page.goto('/roles');
await page.waitForSelector('[data-testid="role-table"]');
await TestHelper.clearAllStorage(page);
});
const endTime = Date.now();
const cleanupTime = endTime - startTime;
console.log(`清理操作时间: ${cleanupTime}ms`);
expect(cleanupTime).toBeLessThan(3000);
});
});
@@ -0,0 +1,488 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';
import { RoleManagementPage } from './pages/RoleManagementPage';
test.describe('性能测试基准', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let userManagementPage: UserManagementPage;
let roleManagementPage: RoleManagementPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
userManagementPage = new UserManagementPage(page);
roleManagementPage = new RoleManagementPage(page);
});
test('登录页面加载性能', async ({ page }) => {
const startTime = Date.now();
await loginPage.goto();
await page.waitForLoadState('networkidle');
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`登录页面加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(3000);
});
test('登录操作性能', async ({ page }) => {
await loginPage.goto();
await page.waitForLoadState('networkidle');
const startTime = Date.now();
await loginPage.usernameInput.fill('admin');
await loginPage.passwordInput.fill('admin123');
await loginPage.loginButton.click();
await page.waitForURL(/.*dashboard/);
const endTime = Date.now();
const loginTime = endTime - startTime;
console.log(`登录操作时间: ${loginTime}ms`);
expect(loginTime).toBeLessThan(2000);
});
test('Dashboard页面加载性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`Dashboard页面加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(2000);
});
test('用户管理页面加载性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await dashboardPage.navigateToUserManagement();
await page.waitForLoadState('networkidle');
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`用户管理页面加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(2000);
});
test('角色管理页面加载性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await dashboardPage.navigateToRoleManagement();
await page.waitForLoadState('networkidle');
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`角色管理页面加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(2000);
});
test('用户列表加载性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await dashboardPage.navigateToUserManagement();
await page.waitForLoadState('networkidle');
const startTime = Date.now();
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`用户列表加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(1500);
});
test('角色列表加载性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await dashboardPage.navigateToRoleManagement();
await page.waitForLoadState('networkidle');
const startTime = Date.now();
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`角色列表加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(1500);
});
test('创建用户对话框打开性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await dashboardPage.navigateToUserManagement();
await page.waitForLoadState('networkidle');
const startTime = Date.now();
await userManagementPage.clickCreateUser();
await expect(page.locator('.el-dialog')).toBeVisible();
const endTime = Date.now();
const openTime = endTime - startTime;
console.log(`创建用户对话框打开时间: ${openTime}ms`);
expect(openTime).toBeLessThan(1000);
});
test('创建角色对话框打开性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await dashboardPage.navigateToRoleManagement();
await page.waitForLoadState('networkidle');
const startTime = Date.now();
await roleManagementPage.clickCreateRole();
await expect(page.locator('.el-dialog')).toBeVisible();
const endTime = Date.now();
const openTime = endTime - startTime;
console.log(`创建角色对话框打开时间: ${openTime}ms`);
expect(openTime).toBeLessThan(1000);
});
test('用户搜索性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await dashboardPage.navigateToUserManagement();
await page.waitForLoadState('networkidle');
const startTime = Date.now();
await userManagementPage.search('admin');
await page.waitForLoadState('networkidle');
const endTime = Date.now();
const searchTime = endTime - startTime;
console.log(`用户搜索时间: ${searchTime}ms`);
expect(searchTime).toBeLessThan(1000);
});
test('角色搜索性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await dashboardPage.navigateToRoleManagement();
await page.waitForLoadState('networkidle');
const startTime = Date.now();
const searchInput = page.locator('input[placeholder*="搜索"]').or(page.locator('.search-input'));
if (await searchInput.count() > 0) {
await searchInput.fill('admin');
await page.waitForLoadState('networkidle');
}
const endTime = Date.now();
const searchTime = endTime - startTime;
console.log(`角色搜索时间: ${searchTime}ms`);
expect(searchTime).toBeLessThan(1000);
});
test('用户表单提交性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await dashboardPage.navigateToUserManagement();
await page.waitForLoadState('networkidle');
await userManagementPage.clickCreateUser();
await expect(page.locator('.el-dialog')).toBeVisible();
const userData = {
username: `testuser_${Date.now()}`,
nickname: '测试用户',
email: 'test@example.com',
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
const startTime = Date.now();
await userManagementPage.submitForm();
await expect(page.locator('.el-message--success')).toBeVisible();
const endTime = Date.now();
const submitTime = endTime - startTime;
console.log(`用户表单提交时间: ${submitTime}ms`);
expect(submitTime).toBeLessThan(2000);
});
test('角色表单提交性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await dashboardPage.navigateToRoleManagement();
await page.waitForLoadState('networkidle');
await roleManagementPage.clickCreateRole();
await expect(page.locator('.el-dialog')).toBeVisible();
const roleData = {
roleName: `测试角色_${Date.now()}`,
roleKey: `test_role_${Date.now()}`,
roleSort: '1',
status: '1',
remark: '测试角色',
};
await roleManagementPage.fillRoleForm(roleData);
const startTime = Date.now();
await roleManagementPage.submitForm();
await expect(page.locator('.el-message--success')).toBeVisible();
const endTime = Date.now();
const submitTime = endTime - startTime;
console.log(`角色表单提交时间: ${submitTime}ms`);
expect(submitTime).toBeLessThan(2000);
});
test('页面切换性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const switchTimes = 5;
const startTime = Date.now();
for (let i = 0; i < switchTimes; i++) {
await dashboardPage.navigateToUserManagement();
await page.waitForLoadState('networkidle');
await dashboardPage.navigateToRoleManagement();
await page.waitForLoadState('networkidle');
}
const endTime = Date.now();
const avgSwitchTime = (endTime - startTime) / switchTimes;
console.log(`平均页面切换时间: ${avgSwitchTime}ms`);
expect(avgSwitchTime).toBeLessThan(1000);
});
test('表格滚动性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await dashboardPage.navigateToUserManagement();
await page.waitForLoadState('networkidle');
const table = page.locator('.el-table').first();
await expect(table).toBeVisible();
const startTime = Date.now();
await table.evaluate(el => {
el.scrollTop = 1000;
});
await page.waitForTimeout(500);
const endTime = Date.now();
const scrollTime = endTime - startTime;
console.log(`表格滚动时间: ${scrollTime}ms`);
expect(scrollTime).toBeLessThan(500);
});
test('内存使用性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const metrics = await page.evaluate(() => {
if (window.performance && (window.performance as any).memory) {
const perfMemory = (window.performance as any).memory;
return {
usedJSHeapSize: perfMemory.usedJSHeapSize,
totalJSHeapSize: perfMemory.totalJSHeapSize,
jsHeapSizeLimit: perfMemory.jsHeapSizeLimit,
};
}
return null;
});
if (metrics) {
console.log('内存使用情况:', metrics);
const memoryUsageRatio = metrics.usedJSHeapSize / metrics.jsHeapSizeLimit;
expect(memoryUsageRatio).toBeLessThan(0.8);
}
});
test('网络请求性能', async ({ page }) => {
const apiRequests: { url: string; duration: number }[] = [];
page.on('response', async (response) => {
if (response.url().includes('/api/')) {
const timing = (response as any).timing();
const duration = timing.responseEnd - timing.requestStart;
apiRequests.push({
url: response.url(),
duration,
});
}
});
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await dashboardPage.navigateToUserManagement();
await page.waitForLoadState('networkidle');
if (apiRequests.length > 0) {
const avgDuration = apiRequests.reduce((sum, req) => sum + req.duration, 0) / apiRequests.length;
const maxDuration = Math.max(...apiRequests.map(req => req.duration));
console.log(`API请求平均时间: ${avgDuration}ms`);
console.log(`API请求最大时间: ${maxDuration}ms`);
expect(avgDuration).toBeLessThan(500);
expect(maxDuration).toBeLessThan(2000);
}
});
test('并发操作性能', async ({ page, context }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
const page1 = page;
const page2 = await context.newPage();
const page3 = await context.newPage();
await Promise.all([
page1.goto('/users'),
page2.goto('/roles'),
page3.goto('/menus'),
]);
await Promise.all([
page1.waitForLoadState('networkidle'),
page2.waitForLoadState('networkidle'),
page3.waitForLoadState('networkidle'),
]);
const endTime = Date.now();
const concurrentLoadTime = endTime - startTime;
console.log(`并发页面加载时间: ${concurrentLoadTime}ms`);
expect(concurrentLoadTime).toBeLessThan(5000);
await page2.close();
await page3.close();
});
test('长时间运行稳定性', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
const duration = 60000; // 1分钟
let operationCount = 0;
const interval = setInterval(async () => {
await page.goto('/users');
await page.waitForLoadState('networkidle');
await page.goto('/roles');
await page.waitForLoadState('networkidle');
operationCount++;
}, 5000);
await page.waitForTimeout(duration);
clearInterval(interval);
const endTime = Date.now();
const actualDuration = endTime - startTime;
console.log(`长时间运行操作次数: ${operationCount}`);
console.log(`长时间运行实际时间: ${actualDuration}ms`);
expect(operationCount).toBeGreaterThan(10);
});
test('响应式布局性能', async ({ page }) => {
await loginPage.goto();
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const viewports = [
{ width: 1920, height: 1080 },
{ width: 1366, height: 768 },
{ width: 768, height: 1024 },
{ width: 375, height: 667 },
];
for (const viewport of viewports) {
const startTime = Date.now();
await page.setViewportSize(viewport);
await page.reload();
await page.waitForLoadState('networkidle');
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`视口 ${viewport.width}x${viewport.height} 加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(3000);
}
});
});
@@ -0,0 +1,417 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';
import { RoleManagementPage } from './pages/RoleManagementPage';
import { TestHelper } from './utils/testHelper';
test.describe('性能优化测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let userManagementPage: UserManagementPage;
let roleManagementPage: RoleManagementPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
userManagementPage = new UserManagementPage(page);
roleManagementPage = new RoleManagementPage(page);
await loginPage.goto();
});
test.afterEach(async ({ page }) => {
await TestHelper.clearAllStorage(page);
});
test.describe('等待策略优化测试', () => {
test('登录页面 - 使用精确等待', async ({ page }) => {
const startTime = Date.now();
await test.step('等待登录页面加载完成', async () => {
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="login-form"]', { state: 'visible' });
});
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`登录页面加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(3000);
});
test('用户列表 - 使用智能等待', async ({ page }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await test.step('等待用户列表加载完成', async () => {
await page.goto('/users');
await page.waitForLoadState('domcontentloaded');
await page.waitForSelector('[data-testid="user-table"]', { state: 'attached' });
await page.waitForSelector('.el-table__body tr', { state: 'visible' });
});
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`用户列表加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(2000);
});
test('角色列表 - 使用条件等待', async ({ page }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await test.step('等待角色列表加载完成', async () => {
await page.goto('/roles');
await page.waitForFunction(() => {
const rows = document.querySelectorAll('.el-table__body tr');
return rows.length > 0;
});
});
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`角色列表加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(2000);
});
});
test.describe('选择器优化测试', () => {
test('使用data-testid选择器', async ({ page }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await test.step('使用data-testid定位元素', async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
const createButton = page.locator('[data-testid="create-user-button"]');
await createButton.click();
await page.waitForSelector('[data-testid="user-form"]');
await page.fill('[data-testid="username-input"]', 'testuser');
await page.fill('[data-testid="password-input"]', 'password123');
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.click('[data-testid="save-button"]');
});
const endTime = Date.now();
const operationTime = endTime - startTime;
console.log(`data-testid选择器操作时间: ${operationTime}ms`);
expect(operationTime).toBeLessThan(3000);
});
test('选择器性能对比', async ({ page }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await test.step('对比不同选择器性能', async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
const startTime1 = Date.now();
const element1 = page.locator('[data-testid="create-user-button"]');
await element1.click();
const time1 = Date.now() - startTime1;
await page.click('.el-button--primary');
await page.waitForTimeout(500);
const startTime2 = Date.now();
const element2 = page.locator('button.el-button--primary');
await element2.click();
const time2 = Date.now() - startTime2;
console.log(`data-testid选择器: ${time1}ms`);
console.log(`CSS选择器: ${time2}ms`);
expect(time1).toBeLessThan(time2);
});
});
});
test.describe('测试数据优化测试', () => {
test('使用缓存数据', async ({ page }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await test.step('首次加载用户列表', async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
});
const firstLoadTime = Date.now() - startTime;
await page.goto('/dashboard');
await page.waitForURL(/.*dashboard/);
const startTime2 = Date.now();
await test.step('再次加载用户列表(使用缓存)', async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
});
const secondLoadTime = Date.now() - startTime2;
console.log(`首次加载时间: ${firstLoadTime}ms`);
console.log(`缓存加载时间: ${secondLoadTime}ms`);
expect(secondLoadTime).toBeLessThan(firstLoadTime);
});
test('优化数据准备时间', async ({ page, request }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await test.step('批量创建用户并测试性能', async () => {
const users = [];
for (let i = 0; i < 10; i++) {
const user = {
username: `perfuser${i}`,
password: 'password123',
email: `perfuser${i}@example.com`,
roleIds: ['1']
};
users.push(user);
await request.post('http://localhost:8084/api/users', {
data: user,
headers: {
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
}
});
}
});
const dataPrepTime = Date.now() - startTime;
const startTime2 = Date.now();
await test.step('加载大量用户数据', async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
await page.waitForFunction(() => {
const rows = document.querySelectorAll('.el-table__body tr');
return rows.length >= 10;
});
});
const loadTime = Date.now() - startTime2;
console.log(`数据准备时间: ${dataPrepTime}ms`);
console.log(`数据加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(5000);
});
});
test.describe('测试隔离优化测试', () => {
test('独立测试环境', async ({ page, context }) => {
const page1 = page;
const page2 = await context.newPage();
await test.step('在独立页面中执行测试', async () => {
await page1.goto('/login');
await page2.goto('/login');
await page1.fill('[data-testid="username-input"]', 'admin');
await page2.fill('[data-testid="username-input"]', 'testuser');
await page1.fill('[data-testid="password-input"]', 'admin123');
await page2.fill('[data-testid="password-input"]', 'password123');
await page1.click('[data-testid="login-button"]');
await page2.click('[data-testid="login-button"]');
await page1.waitForURL(/.*dashboard/);
await page2.waitForURL(/.*dashboard/);
});
await test.step('验证页面隔离', async () => {
const url1 = page1.url();
const url2 = page2.url();
expect(url1).toContain('/dashboard');
expect(url2).toContain('/dashboard');
expect(url1).not.toBe(url2);
});
});
test('测试清理优化', async ({ page, request }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await test.step('创建测试数据', async () => {
const user = {
username: 'cleanupuser',
password: 'password123',
email: 'cleanup@example.com',
roleIds: ['1']
};
await request.post('http://localhost:8084/api/users', {
data: user,
headers: {
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
}
});
});
const createTime = Date.now() - startTime;
const startTime2 = Date.now();
await test.step('快速清理测试数据', async () => {
const usersResponse = await request.get('http://localhost:8084/api/users', {
headers: {
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
}
});
const usersData = await usersResponse.json();
const cleanupUser = usersData.find(u => u.username === 'cleanupuser');
if (cleanupUser) {
await request.delete(`http://localhost:8084/api/users/${cleanupUser.id}`, {
headers: {
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
}
});
}
});
const cleanupTime = Date.now() - startTime2;
console.log(`数据创建时间: ${createTime}ms`);
console.log(`数据清理时间: ${cleanupTime}ms`);
expect(cleanupTime).toBeLessThan(1000);
});
});
test.describe('并行化优化测试', () => {
test('并行执行多个测试', async ({ page }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await test.step('并行加载多个页面', async () => {
const promises = [
page.goto('/users'),
page.goto('/roles'),
page.goto('/settings')
];
await Promise.all(promises);
});
const endTime = Date.now();
const parallelTime = endTime - startTime;
console.log(`并行加载时间: ${parallelTime}ms`);
expect(parallelTime).toBeLessThan(5000);
});
test('并发API请求', async ({ page, request }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const startTime = Date.now();
await test.step('并发发送多个API请求', async () => {
const promises = [
request.get('http://localhost:8084/api/users', {
headers: {
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
}
}),
request.get('http://localhost:8084/api/roles', {
headers: {
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
}
}),
request.get('http://localhost:8084/api/permissions', {
headers: {
'Authorization': `Bearer ${await TestHelper.getAuthToken(page)}`
}
})
];
await Promise.all(promises);
});
const endTime = Date.now();
const concurrentTime = endTime - startTime;
console.log(`并发请求时间: ${concurrentTime}ms`);
expect(concurrentTime).toBeLessThan(2000);
});
});
test.describe('内存和资源优化测试', () => {
test('内存使用监控', async ({ page }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
const initialMemory = await page.evaluate(() => {
if ((window.performance as any).memory) {
return (window.performance as any).memory.usedJSHeapSize;
}
return 0;
});
await test.step('执行多个操作', async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
await page.goto('/roles');
await page.waitForSelector('[data-testid="role-table"]');
await page.goto('/settings');
await page.waitForSelector('[data-testid="settings-form"]');
});
const finalMemory = await page.evaluate(() => {
if ((window.performance as any).memory) {
return (window.performance as any).memory.usedJSHeapSize;
}
return 0;
});
const memoryIncrease = finalMemory - initialMemory;
const memoryIncreaseMB = memoryIncrease / (1024 * 1024);
console.log(`内存增长: ${memoryIncreaseMB.toFixed(2)}MB`);
expect(memoryIncreaseMB).toBeLessThan(50);
});
test('DOM节点数量监控', async ({ page }) => {
await loginPage.login('admin', 'admin123');
await page.waitForURL(/.*dashboard/);
await test.step('监控DOM节点数量', async () => {
await page.goto('/users');
await page.waitForSelector('[data-testid="user-table"]');
const nodeCount = await page.evaluate(() => {
return document.querySelectorAll('*').length;
});
console.log(`DOM节点数量: ${nodeCount}`);
expect(nodeCount).toBeLessThan(5000);
});
});
});
});
@@ -0,0 +1,339 @@
#!/usr/bin/env node
/**
* 性能监控工具
* 收集和分析测试性能数据,识别性能瓶颈
*/
const fs = require('fs');
const path = require('path');
class PerformanceMonitor {
constructor() {
this.performanceDataPath = path.join(process.cwd(), 'test-results', 'performance-data.json');
this.performanceData = this.loadPerformanceData();
this.currentSession = {
startTime: Date.now(),
tests: [],
metrics: {}
};
}
loadPerformanceData() {
try {
if (fs.existsSync(this.performanceDataPath)) {
return JSON.parse(fs.readFileSync(this.performanceDataPath, 'utf-8'));
}
} catch (error) {
console.warn('加载性能数据失败:', error.message);
}
return {
sessions: [],
summary: {
avgTestTime: 0,
avgPageLoadTime: 0,
avgApiTime: 0,
totalTests: 0,
slowTests: [],
fastTests: []
}
};
}
savePerformanceData() {
const dir = path.dirname(this.performanceDataPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.performanceDataPath, JSON.stringify(this.performanceData, null, 2), 'utf-8');
}
startTest(testName) {
const test = {
name: testName,
startTime: Date.now(),
metrics: {
pageLoads: [],
apiCalls: [],
domOperations: []
}
};
this.currentSession.tests.push(test);
return test;
}
endTest(test) {
test.endTime = Date.now();
test.duration = test.endTime - test.startTime;
return test;
}
recordPageLoad(test, url, loadTime) {
test.metrics.pageLoads.push({
url,
loadTime,
timestamp: Date.now()
});
}
recordApiCall(test, endpoint, duration) {
test.metrics.apiCalls.push({
endpoint,
duration,
timestamp: Date.now()
});
}
recordDomOperation(test, operation, duration) {
test.metrics.domOperations.push({
operation,
duration,
timestamp: Date.now()
});
}
endSession() {
this.currentSession.endTime = Date.now();
this.currentSession.duration = this.currentSession.endTime - this.currentSession.startTime;
this.performanceData.sessions.push(this.currentSession);
this.updateSummary();
this.savePerformanceData();
return this.currentSession;
}
updateSummary() {
const sessions = this.performanceData.sessions;
const allTests = sessions.flatMap(s => s.tests);
if (allTests.length === 0) return;
const totalDuration = allTests.reduce((sum, t) => sum + t.duration, 0);
const avgTestTime = totalDuration / allTests.length;
const allPageLoads = allTests.flatMap(t => t.metrics.pageLoads);
const avgPageLoadTime = allPageLoads.length > 0
? allPageLoads.reduce((sum, p) => sum + p.loadTime, 0) / allPageLoads.length
: 0;
const allApiCalls = allTests.flatMap(t => t.metrics.apiCalls);
const avgApiTime = allApiCalls.length > 0
? allApiCalls.reduce((sum, a) => sum + a.duration, 0) / allApiCalls.length
: 0;
const sortedTests = [...allTests].sort((a, b) => b.duration - a.duration);
const slowTests = sortedTests.slice(0, 10);
const fastTests = sortedTests.slice(-10).reverse();
this.performanceData.summary = {
avgTestTime,
avgPageLoadTime,
avgApiTime,
totalTests: allTests.length,
slowTests: slowTests.map(t => ({
name: t.name,
duration: t.duration
})),
fastTests: fastTests.map(t => ({
name: t.name,
duration: t.duration
}))
};
}
generateReport() {
const summary = this.performanceData.summary;
const sessions = this.performanceData.sessions;
console.log('');
console.log('═══════════════════════════════════════════');
console.log('📊 性能监控报告');
console.log('═══════════════════════════════════════════');
console.log('');
console.log(`📈 总测试数: ${summary.totalTests}`);
console.log(`⏱️ 平均测试时间: ${this.formatDuration(summary.avgTestTime)}`);
console.log(`🌐 平均页面加载时间: ${this.formatDuration(summary.avgPageLoadTime)}`);
console.log(`📡 平均API响应时间: ${this.formatDuration(summary.avgApiTime)}`);
console.log('');
if (summary.slowTests.length > 0) {
console.log('🐌 最慢的10个测试:');
summary.slowTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.name} - ${this.formatDuration(test.duration)}`);
});
console.log('');
}
if (summary.fastTests.length > 0) {
console.log('⚡ 最快的10个测试:');
summary.fastTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.name} - ${this.formatDuration(test.duration)}`);
});
console.log('');
}
this.analyzePerformanceTrends();
this.generateRecommendations();
}
analyzePerformanceTrends() {
const sessions = this.performanceData.sessions;
if (sessions.length < 2) return;
const recentSessions = sessions.slice(-5);
const avgDurations = recentSessions.map(s => {
const tests = s.tests;
if (tests.length === 0) return 0;
return tests.reduce((sum, t) => sum + t.duration, 0) / tests.length;
});
const trend = this.calculateTrend(avgDurations);
console.log('📈 性能趋势:');
console.log(` 趋势: ${this.getTrendEmoji(trend)} ${trend.toUpperCase()}`);
console.log(` 最近5次平均测试时间: ${avgDurations.map(d => this.formatDuration(d)).join(', ')}`);
console.log('');
}
calculateTrend(values) {
if (values.length < 2) return 'stable';
const firstHalf = values.slice(0, Math.floor(values.length / 2));
const secondHalf = values.slice(Math.floor(values.length / 2));
const firstAvg = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
const secondAvg = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length;
const change = ((secondAvg - firstAvg) / firstAvg) * 100;
if (change < -10) return 'improving';
if (change > 10) return 'degrading';
return 'stable';
}
generateRecommendations() {
const summary = this.performanceData.summary;
const recommendations = [];
if (summary.avgTestTime > 5000) {
recommendations.push('⚠️ 平均测试时间超过5秒,建议优化测试执行效率');
}
if (summary.avgPageLoadTime > 2000) {
recommendations.push('⚠️ 平均页面加载时间超过2秒,建议优化页面性能');
}
if (summary.avgApiTime > 1000) {
recommendations.push('⚠️ 平均API响应时间超过1秒,建议优化API性能');
}
const slowTestsCount = summary.slowTests.filter(t => t.duration > 10000).length;
if (slowTestsCount > 5) {
recommendations.push(`⚠️ 有${slowTestsCount}个测试执行时间超过10秒,建议重点优化`);
}
if (recommendations.length > 0) {
console.log('💡 性能优化建议:');
recommendations.forEach(rec => {
console.log(` ${rec}`);
});
console.log('');
}
}
getTrendEmoji(trend) {
switch (trend) {
case 'improving':
return '📈';
case 'degrading':
return '📉';
default:
return '➡️';
}
}
formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
return `${(ms / 60000).toFixed(1)}m`;
}
}
exportData(filePath) {
const exportPath = filePath || 'performance-data-export.json';
fs.writeFileSync(exportPath, JSON.stringify(this.performanceData, null, 2), 'utf-8');
console.log(`✅ 性能数据已导出到: ${exportPath}`);
}
}
// 命令行接口
if (require.main === module) {
const monitor = new PerformanceMonitor();
const command = process.argv[2];
switch (command) {
case 'report':
monitor.generateReport();
break;
case 'export':
const exportFile = process.argv[3];
monitor.exportData(exportFile);
break;
case 'start':
const testName = process.argv[3];
if (testName) {
const test = monitor.startTest(testName);
console.log(`✅ 测试已启动: ${testName}`);
console.log(`测试ID: ${monitor.currentSession.tests.length - 1}`);
} else {
console.error('❌ 错误: 请提供测试名称');
process.exit(1);
}
break;
case 'end':
const testId = parseInt(process.argv[3]);
if (!isNaN(testId)) {
const test = monitor.currentSession.tests[testId];
if (test) {
monitor.endTest(test);
console.log(`✅ 测试已结束: ${test.name}`);
console.log(`执行时间: ${monitor.formatDuration(test.duration)}`);
} else {
console.error('❌ 错误: 测试ID不存在');
process.exit(1);
}
} else {
console.error('❌ 错误: 请提供有效的测试ID');
process.exit(1);
}
break;
case 'session':
monitor.endSession();
console.log('✅ 测试会话已结束');
console.log(`会话时长: ${monitor.formatDuration(monitor.currentSession.duration)}`);
console.log(`测试数量: ${monitor.currentSession.tests.length}`);
break;
default:
console.log('性能监控工具');
console.log('');
console.log('用法:');
console.log(' node performanceMonitor.js report - 生成性能报告');
console.log(' node performanceMonitor.js export [file.json] - 导出性能数据');
console.log(' node performanceMonitor.js start <testName> - 启动测试监控');
console.log(' node performanceMonitor.js end <testId> - 结束测试监控');
console.log(' node performanceMonitor.js session - 结束测试会话');
console.log('');
break;
}
}
module.exports = PerformanceMonitor;
+346
View File
@@ -0,0 +1,346 @@
#!/usr/bin/env node
/**
* 质量门禁检查工具
* 定义和执行自动化质量标准,阻止低质量代码合并
*/
const fs = require('fs');
const path = require('path');
class QualityGate {
constructor() {
this.qualityStandards = {
passRate: 95, // 通过率必须 >= 95%
flakyRate: 5, // 不稳定测试比例必须 <= 5%
maxDuration: 600000, // 总测试时间必须 <= 10分钟
maxFailedTests: 5, // 失败测试数量必须 <= 5
maxSlowTests: 10, // 慢速测试数量必须 <= 10
};
this.checks = [];
this.passed = true;
this.warnings = [];
this.errors = [];
}
checkPassRate(results) {
const passRate = results.summary?.passRate || 0;
const threshold = this.qualityStandards.passRate;
if (passRate < threshold) {
this.errors.push({
check: '通过率检查',
message: `测试通过率 ${passRate.toFixed(2)}% 低于标准 ${threshold}%`,
actual: passRate,
threshold: threshold,
status: 'failed',
});
this.passed = false;
} else {
this.checks.push({
check: '通过率检查',
message: `测试通过率 ${passRate.toFixed(2)}% 符合标准`,
actual: passRate,
threshold: threshold,
status: 'passed',
});
}
}
checkFlakyRate(results) {
const flakyRate = results.summary?.flakyRate || 0;
const threshold = this.qualityStandards.flakyRate;
if (flakyRate > threshold) {
this.warnings.push({
check: '不稳定测试检查',
message: `不稳定测试比例 ${flakyRate.toFixed(2)}% 超过标准 ${threshold}%`,
actual: flakyRate,
threshold: threshold,
status: 'warning',
});
} else {
this.checks.push({
check: '不稳定测试检查',
message: `不稳定测试比例 ${flakyRate.toFixed(2)}% 符合标准`,
actual: flakyRate,
threshold: threshold,
status: 'passed',
});
}
}
checkDuration(results) {
const duration = results.summary?.totalDuration || 0;
const threshold = this.qualityStandards.maxDuration;
if (duration > threshold) {
this.warnings.push({
check: '测试耗时检查',
message: `测试总耗时 ${this.formatDuration(duration)} 超过标准 ${this.formatDuration(threshold)}`,
actual: duration,
threshold: threshold,
status: 'warning',
});
} else {
this.checks.push({
check: '测试耗时检查',
message: `测试总耗时 ${this.formatDuration(duration)} 符合标准`,
actual: duration,
threshold: threshold,
status: 'passed',
});
}
}
checkFailedTests(results) {
const failedCount = results.failedTests?.length || 0;
const threshold = this.qualityStandards.maxFailedTests;
if (failedCount > threshold) {
this.errors.push({
check: '失败测试数量检查',
message: `失败测试数量 ${failedCount} 超过标准 ${threshold}`,
actual: failedCount,
threshold: threshold,
status: 'failed',
});
this.passed = false;
} else {
this.checks.push({
check: '失败测试数量检查',
message: `失败测试数量 ${failedCount} 符合标准`,
actual: failedCount,
threshold: threshold,
status: 'passed',
});
}
}
checkSlowTests(results) {
const slowCount = results.slowestTests?.length || 0;
const threshold = this.qualityStandards.maxSlowTests;
if (slowCount > threshold) {
this.warnings.push({
check: '慢速测试数量检查',
message: `慢速测试数量 ${slowCount} 超过标准 ${threshold}`,
actual: slowCount,
threshold: threshold,
status: 'warning',
});
} else {
this.checks.push({
check: '慢速测试数量检查',
message: `慢速测试数量 ${slowCount} 符合标准`,
actual: slowCount,
threshold: threshold,
status: 'passed',
});
}
}
checkCriticalTests(results) {
const criticalTests = results.failedTests?.filter(test => {
const title = test.title.toLowerCase();
return title.includes('登录') || title.includes('认证') || title.includes('安全');
}) || [];
if (criticalTests.length > 0) {
this.errors.push({
check: '关键功能测试检查',
message: `关键功能测试失败: ${criticalTests.map(t => t.title).join(', ')}`,
actual: criticalTests.length,
threshold: 0,
status: 'failed',
});
this.passed = false;
} else {
this.checks.push({
check: '关键功能测试检查',
message: '所有关键功能测试通过',
actual: 0,
threshold: 0,
status: 'passed',
});
}
}
execute(results) {
this.checkPassRate(results);
this.checkFlakyRate(results);
this.checkDuration(results);
this.checkFailedTests(results);
this.checkSlowTests(results);
this.checkCriticalTests(results);
return this.generateReport();
}
generateReport() {
const report = {
timestamp: new Date().toISOString(),
passed: this.passed,
summary: {
total: this.checks.length,
passed: this.checks.filter(c => c.status === 'passed').length,
warnings: this.warnings.length,
errors: this.errors.length,
},
checks: this.checks,
warnings: this.warnings,
errors: this.errors,
};
this.printReport(report);
this.saveReport(report);
return report;
}
printReport(report) {
console.log('');
console.log('═══════════════════════════════════════════');
console.log('🚪 质量门禁检查报告');
console.log('═══════════════════════════════════════════');
console.log('');
console.log(`📊 检查时间: ${new Date(report.timestamp).toLocaleString('zh-CN')}`);
console.log(`📈 检查结果: ${report.passed ? '✅ 通过' : '❌ 失败'}`);
console.log('');
console.log(`📋 检查统计:`);
console.log(` - 总检查项: ${report.summary.total}`);
console.log(` - 通过: ${report.summary.passed}`);
console.log(` - 警告: ${report.summary.warnings}`);
console.log(` - 错误: ${report.summary.errors}`);
console.log('');
if (report.checks.length > 0) {
console.log('✅ 通过的检查:');
report.checks.forEach(check => {
console.log(`${check.check}: ${check.message}`);
});
console.log('');
}
if (report.warnings.length > 0) {
console.log('⚠️ 警告:');
report.warnings.forEach(warning => {
console.log(` ⚠️ ${warning.check}: ${warning.message}`);
});
console.log('');
}
if (report.errors.length > 0) {
console.log('❌ 错误:');
report.errors.forEach(error => {
console.log(`${error.check}: ${error.message}`);
});
console.log('');
}
console.log('═══════════════════════════════════════════');
console.log('');
if (!report.passed) {
console.error('❌ 质量门禁检查失败!请修复错误后重试。');
process.exit(1);
}
}
saveReport(report) {
const dir = path.join(process.cwd(), 'test-results');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const reportPath = path.join(dir, 'quality-gate-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
console.log(`📄 质量门禁报告已保存: ${reportPath}`);
}
formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
return `${(ms / 60000).toFixed(1)}m`;
}
}
setStandard(standard, value) {
if (this.qualityStandards.hasOwnProperty(standard)) {
this.qualityStandards[standard] = value;
console.log(`✅ 质量标准已更新: ${standard} = ${value}`);
} else {
console.error(`❌ 错误: 未知的质量标准 ${standard}`);
process.exit(1);
}
}
getStandards() {
console.log('当前质量标准:');
console.log('');
console.log(` 通过率: >= ${this.qualityStandards.passRate}%`);
console.log(` 不稳定测试比例: <= ${this.qualityStandards.flakyRate}%`);
console.log(` 最大测试时间: <= ${this.formatDuration(this.qualityStandards.maxDuration)}`);
console.log(` 最大失败测试数: <= ${this.qualityStandards.maxFailedTests}`);
console.log(` 最大慢速测试数: <= ${this.qualityStandards.maxSlowTests}`);
console.log('');
}
}
// 命令行接口
if (require.main === module) {
const qualityGate = new QualityGate();
const command = process.argv[2];
switch (command) {
case 'check':
const resultsFile = process.argv[3];
if (resultsFile && fs.existsSync(resultsFile)) {
const results = JSON.parse(fs.readFileSync(resultsFile, 'utf-8'));
qualityGate.execute(results);
} else {
console.error('❌ 错误: 请提供有效的测试结果文件');
process.exit(1);
}
break;
case 'set':
const standard = process.argv[3];
const value = parseFloat(process.argv[4]);
if (standard && !isNaN(value)) {
qualityGate.setStandard(standard, value);
} else {
console.error('❌ 错误: 请提供有效的标准和数值');
console.error('用法: node qualityGate.js set <standard> <value>');
process.exit(1);
}
break;
case 'standards':
qualityGate.getStandards();
break;
default:
console.log('质量门禁检查工具');
console.log('');
console.log('用法:');
console.log(' node qualityGate.js check <results.json> - 执行质量门禁检查');
console.log(' node qualityGate.js set <standard> <value> - 设置质量标准');
console.log(' node qualityGate.js standards - 显示当前质量标准');
console.log('');
console.log('质量标准:');
console.log(' - passRate: 通过率 (%)');
console.log(' - flakyRate: 不稳定测试比例 (%)');
console.log(' - maxDuration: 最大测试时间 (ms)');
console.log(' - maxFailedTests: 最大失败测试数');
console.log(' - maxSlowTests: 最大慢速测试数');
console.log('');
break;
}
}
module.exports = QualityGate;
@@ -0,0 +1,386 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { RoleManagementPage } from './pages/RoleManagementPage';
import { TestDataManager } from './utils/testDataManager';
import { TestHelper } from './utils/testHelper';
test.describe('角色管理异常场景测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let roleManagementPage: RoleManagementPage;
let testRole: any;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
roleManagementPage = new RoleManagementPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
});
test.afterEach(async ({ page, request }) => {
await TestHelper.clearAllStorage(page);
if (testRole) {
await TestDataManager.deleteTestRole(request, testRole.roleKey);
testRole = null;
}
});
test('创建角色 - 重复角色键', async ({ page }) => {
await test.step('导航到角色管理页面', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试创建重复角色键的角色', async () => {
await roleManagementPage.clickCreateRole();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const roleData = {
roleName: '管理员',
roleKey: 'admin',
roleSort: '1',
status: '1',
remark: '重复角色键',
};
await roleManagementPage.fillRoleForm(roleData);
await roleManagementPage.submitForm();
});
await test.step('验证错误消息', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('角色键已存在');
});
});
test('创建角色 - 缺少必填字段', async ({ page }) => {
await test.step('导航到角色管理页面', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试创建缺少必填字段的角色', async () => {
await roleManagementPage.clickCreateRole();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const roleData = {
roleName: '',
roleKey: '',
roleSort: '',
status: '',
remark: '',
};
await roleManagementPage.fillRoleForm(roleData);
await roleManagementPage.submitForm();
});
await test.step('验证表单验证', async () => {
const submitButton = page.locator('.el-dialog__footer button[type="submit"]');
const isDisabled = await submitButton.evaluate(el => (el as HTMLButtonElement).disabled);
expect(isDisabled).toBeTruthy();
});
});
test('创建角色 - 无效角色键格式', async ({ page }) => {
await test.step('导航到角色管理页面', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试创建无效角色键格式的角色', async () => {
await roleManagementPage.clickCreateRole();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const roleData = {
roleName: `测试角色_${Date.now()}`,
roleKey: '无效角色键!@#',
roleSort: '1',
status: '1',
remark: '无效角色键格式',
};
await roleManagementPage.fillRoleForm(roleData);
await roleManagementPage.submitForm();
});
await test.step('验证角色键格式错误', async () => {
const roleKeyInput = page.locator('input[name="roleKey"]');
const hasError = await roleKeyInput.evaluate(el => el.classList.contains('is-error'));
expect(hasError).toBeTruthy();
});
});
test('编辑角色 - 不存在的角色ID', async ({ page }) => {
await test.step('尝试编辑不存在的角色', async () => {
await page.goto('/roles/999999/edit');
await TestHelper.waitForPageLoad(page);
});
await test.step('验证404错误或重定向', async () => {
const currentUrl = page.url();
expect(currentUrl).toMatch(/(404|roles)/);
});
});
test('删除角色 - 不存在的角色ID', async ({ page, request }) => {
await test.step('尝试删除不存在的角色', async () => {
const response = await request.delete('http://localhost:8084/api/roles/999999');
expect(response.status()).toBe(404);
});
});
test('删除角色 - 系统内置角色', async ({ page }) => {
await test.step('导航到角色管理页面', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试删除系统内置角色', async () => {
const adminRoleRow = page.locator('table tbody tr').filter({ hasText: 'admin' }).first();
const deleteButton = adminRoleRow.locator('.delete-button');
if (await deleteButton.count() > 0) {
await deleteButton.click();
await TestHelper.waitForElementVisible(page, '.el-message-box');
await page.click('.el-message-box__btns .el-button--primary');
await TestHelper.waitForPageLoad(page);
}
});
await test.step('验证系统内置角色不能删除', async () => {
const adminRoleRow = page.locator('table tbody tr').filter({ hasText: 'admin' }).first();
await expect(adminRoleRow).toBeVisible();
});
});
test('搜索角色 - 空搜索条件', async ({ page }) => {
await test.step('导航到角色管理页面', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('执行空搜索', async () => {
const searchInput = page.locator('input[placeholder*="搜索"]').or(page.locator('.search-input'));
if (await searchInput.count() > 0) {
await searchInput.fill('');
await TestHelper.waitForPageLoad(page);
}
});
await test.step('验证显示所有角色', async () => {
const roleCount = await page.locator('.el-table__body tr').count();
expect(roleCount).toBeGreaterThan(0);
});
});
test('搜索角色 - 不存在的角色名', async ({ page }) => {
await test.step('导航到角色管理页面', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('搜索不存在的角色', async () => {
const searchInput = page.locator('input[placeholder*="搜索"]').or(page.locator('.search-input'));
if (await searchInput.count() > 0) {
await searchInput.fill('nonexistentrole123456');
await TestHelper.waitForPageLoad(page);
}
});
await test.step('验证无结果', async () => {
const roleCount = await page.locator('.el-table__body tr').count();
expect(roleCount).toBe(0);
});
});
test('分配权限 - 角色不存在', async ({ page, request }) => {
await test.step('尝试为不存在的角色分配权限', async () => {
const response = await request.post('http://localhost:8084/api/roles/999999/permissions', {
data: { permissions: ['user:view'] }
});
expect(response.status()).toBe(404);
});
});
test('分配权限 - 无效权限标识', async ({ page }) => {
await test.step('导航到角色管理页面', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试分配无效权限', async () => {
const firstRow = page.locator('.el-table__body tr').first();
await firstRow.click();
await TestHelper.waitForElementVisible(page, '.permission-dialog');
const invalidPermission = page.locator('.permission-item').filter({ hasText: 'invalid:permission' });
if (await invalidPermission.count() > 0) {
await invalidPermission.click();
await page.click('.permission-dialog .save-button');
await TestHelper.waitForPageLoad(page);
}
});
await test.step('验证权限分配失败', async () => {
await TestHelper.waitForErrorMessage(page);
});
});
test('角色状态切换 - 禁用后用户无法登录', async ({ page, request }) => {
testRole = TestDataManager.generateTestRole();
await TestDataManager.createTestRole(request, testRole);
const testUser = TestDataManager.generateTestUser({ roleIds: [testRole.id] });
await TestDataManager.createTestUser(request, testUser);
await test.step('禁用角色', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
const roleRow = page.locator('table tbody tr').filter({ hasText: testRole.roleName }).first();
await roleRow.locator('.status-toggle').click();
await TestHelper.waitForSuccessMessage(page);
});
await test.step('验证用户无法登录', async () => {
await loginPage.logout();
await loginPage.goto();
const testUser = TestDataManager.generateTestUser();
await loginPage.login(testUser.username, testUser.password);
await TestHelper.waitForErrorMessage(page);
const currentUrl = page.url();
expect(currentUrl).toContain('/login');
});
});
test('批量删除角色 - 未选择角色', async ({ page }) => {
await test.step('导航到角色管理页面', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试批量删除未选择的角色', async () => {
const batchDeleteButton = page.locator('button:has-text("批量删除")');
if (await batchDeleteButton.count() > 0) {
await batchDeleteButton.click();
}
});
await test.step('验证提示消息', async () => {
await TestHelper.waitForErrorMessage(page);
});
});
test('批量删除角色 - 包含系统内置角色', async ({ page }) => {
await test.step('导航到角色管理页面', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('选择包含系统内置角色的多个角色', async () => {
const adminRoleRow = page.locator('table tbody tr').filter({ hasText: 'admin' }).first();
await adminRoleRow.locator('input[type="checkbox"]').check();
const otherRoleRow = page.locator('table tbody tr').nth(1);
if (await otherRoleRow.count() > 0) {
await otherRoleRow.locator('input[type="checkbox"]').check();
}
});
await test.step('尝试批量删除', async () => {
const batchDeleteButton = page.locator('button:has-text("批量删除")');
if (await batchDeleteButton.count() > 0) {
await batchDeleteButton.click();
await TestHelper.waitForElementVisible(page, '.el-message-box');
await page.click('.el-message-box__btns .el-button--primary');
await TestHelper.waitForPageLoad(page);
}
});
await test.step('验证系统内置角色未被删除', async () => {
const adminRoleRow = page.locator('table tbody tr').filter({ hasText: 'admin' }).first();
await expect(adminRoleRow).toBeVisible();
});
});
test('网络错误 - 创建角色时断网', async ({ page }) => {
await test.step('导航到角色管理页面', async () => {
await dashboardPage.navigateToRoleManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('模拟网络错误', async () => {
await page.route('**/api/roles', route => route.abort('failed'));
});
await test.step('尝试创建角色', async () => {
await roleManagementPage.clickCreateRole();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const roleData = {
roleName: `测试角色_${Date.now()}`,
roleKey: `test_role_${Date.now()}`,
roleSort: '1',
status: '1',
remark: '测试角色',
};
await roleManagementPage.fillRoleForm(roleData);
await roleManagementPage.submitForm();
});
await test.step('验证网络错误提示', async () => {
await TestHelper.waitForErrorMessage(page);
});
});
test('并发操作 - 同时编辑同一角色', async ({ page, context }) => {
const page1 = page;
const page2 = await context.newPage();
await page1.goto('/roles');
await page2.goto('/roles');
await TestHelper.waitForPageLoad(page1);
await TestHelper.waitForPageLoad(page2);
const firstRow1 = page1.locator('.el-table__body tr').first();
const firstRow2 = page2.locator('.el-table__body tr').first();
await firstRow1.click();
await firstRow2.click();
await TestHelper.waitForElementVisible(page1, '.el-dialog');
await TestHelper.waitForElementVisible(page2, '.el-dialog');
await page1.fill('input[name="roleName"]', '并发编辑1');
await page2.fill('input[name="roleName"]', '并发编辑2');
await page1.click('.el-dialog__footer button[type="submit"]');
await TestHelper.waitForPageLoad(page1);
await page2.click('.el-dialog__footer button[type="submit"]');
await TestHelper.waitForPageLoad(page2);
await page1.goto('/roles');
await page2.goto('/roles');
await TestHelper.waitForPageLoad(page1);
await TestHelper.waitForPageLoad(page2);
const errorMessage1 = await TestHelper.getElementText(page1, '.el-message__content');
const errorMessage2 = await TestHelper.getElementText(page2, '.el-message__content');
expect(errorMessage1 || errorMessage2).toContain('数据已被其他用户修改');
});
});
+369
View File
@@ -0,0 +1,369 @@
#!/usr/bin/env node
/**
* 测试趋势分析工具
* 收集和分析历史测试数据,识别测试质量变化趋势
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
class TestTrendAnalyzer {
constructor() {
this.trendDataPath = path.join(process.cwd(), 'test-results', 'trends.json');
this.historyDataPath = path.join(process.cwd(), 'test-results', 'history');
this.trendData = this.loadTrendData();
}
loadTrendData() {
try {
if (fs.existsSync(this.trendDataPath)) {
return JSON.parse(fs.readFileSync(this.trendDataPath, 'utf-8'));
}
} catch (error) {
console.warn('加载趋势数据失败:', error.message);
}
return {
runs: [],
summary: {
totalRuns: 0,
avgPassRate: 0,
avgDuration: 0,
trend: 'stable',
lastUpdated: null,
},
};
}
saveTrendData() {
const dir = path.dirname(this.trendDataPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.trendDataPath, JSON.stringify(this.trendData, null, 2), 'utf-8');
}
addTestRun(testResults) {
const run = {
timestamp: new Date().toISOString(),
summary: {
total: testResults.summary?.total || 0,
passed: testResults.summary?.passed || 0,
failed: testResults.summary?.failed || 0,
skipped: testResults.summary?.skipped || 0,
flaky: testResults.summary?.flaky || 0,
passRate: testResults.summary?.passRate || 0,
failRate: testResults.summary?.failRate || 0,
skipRate: testResults.summary?.skipRate || 0,
flakyRate: testResults.summary?.flakyRate || 0,
totalDuration: testResults.summary?.totalDuration || 0,
avgDuration: testResults.summary?.avgDuration || 0,
},
failedTests: testResults.failedTests || [],
slowestTests: testResults.slowestTests || [],
environment: this.getEnvironmentInfo(),
};
this.trendData.runs.push(run);
this.updateSummary();
this.saveTrendData();
this.saveHistory(run);
return run;
}
updateSummary() {
const runs = this.trendData.runs;
const recentRuns = runs.slice(-10);
this.trendData.summary.totalRuns = runs.length;
this.trendData.summary.avgPassRate = this.calculateAverage(recentRuns, 'passRate');
this.trendData.summary.avgDuration = this.calculateAverage(recentRuns, 'totalDuration');
this.trendData.summary.trend = this.analyzeTrend();
this.trendData.summary.lastUpdated = new Date().toISOString();
}
calculateAverage(runs, field) {
if (runs.length === 0) return 0;
const sum = runs.reduce((acc, run) => acc + (run.summary[field] || 0), 0);
return sum / runs.length;
}
analyzeTrend() {
const runs = this.trendData.runs;
if (runs.length < 3) return 'stable';
const recentPassRates = runs.slice(-5).map(r => r.summary.passRate);
const avgPassRate = recentPassRates.reduce((a, b) => a + b, 0) / recentPassRates.length;
const latestPassRate = recentPassRates[recentPassRates.length - 1];
if (latestPassRate < avgPassRate - 5) {
return 'degrading';
} else if (latestPassRate > avgPassRate + 5) {
return 'improving';
} else {
return 'stable';
}
}
saveHistory(run) {
const dir = this.historyDataPath;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const filename = `run-${Date.now()}.json`;
const filepath = path.join(dir, filename);
fs.writeFileSync(filepath, JSON.stringify(run, null, 2), 'utf-8');
}
getEnvironmentInfo() {
return {
platform: os.platform(),
arch: os.arch(),
nodeVersion: process.version,
hostname: os.hostname(),
cpus: os.cpus().length,
totalMemory: os.totalmem(),
freeMemory: os.freemem(),
};
}
generateTrendReport() {
const runs = this.trendData.runs;
const summary = this.trendData.summary;
if (runs.length === 0) {
console.log('暂无测试数据');
return;
}
console.log('');
console.log('═══════════════════════════════════════════');
console.log('📈 测试趋势分析报告');
console.log('═══════════════════════════════════════════');
console.log('');
console.log(`📊 总运行次数: ${summary.totalRuns}`);
console.log(`📈 平均通过率: ${summary.avgPassRate.toFixed(2)}%`);
console.log(`⏱️ 平均耗时: ${this.formatDuration(summary.avgDuration)}`);
console.log(`📉 趋势: ${this.getTrendEmoji(summary.trend)} ${summary.trend.toUpperCase()}`);
console.log('');
const recentRuns = runs.slice(-10);
console.log('📅 最近10次运行:');
recentRuns.forEach((run, index) => {
const date = new Date(run.timestamp);
const dateStr = date.toLocaleString('zh-CN');
const passRate = run.summary.passRate.toFixed(2);
const duration = this.formatDuration(run.summary.totalDuration);
console.log(` ${index + 1}. ${dateStr} - 通过率: ${passRate}% - 耗时: ${duration}`);
});
console.log('');
this.analyzeFlakyTests();
this.analyzeSlowTests();
this.analyzeFailedTests();
this.generateRecommendations();
}
analyzeFlakyTests() {
const runs = this.trendData.runs;
const flakyTestMap = new Map();
runs.forEach(run => {
run.failedTests.forEach(test => {
const key = `${test.title}`;
if (!flakyTestMap.has(key)) {
flakyTestMap.set(key, {
title: test.title,
failures: 0,
runs: 0,
});
}
flakyTestMap.get(key).failures++;
});
flakyTestMap.forEach(test => {
test.runs++;
});
});
const flakyTests = Array.from(flakyTestMap.values())
.filter(test => test.failures >= 2)
.sort((a, b) => b.failures - a.failures)
.slice(0, 10);
if (flakyTests.length > 0) {
console.log('🔄 不稳定测试 (失败2次以上):');
flakyTests.forEach((test, index) => {
const failRate = ((test.failures / test.runs) * 100).toFixed(2);
console.log(` ${index + 1}. ${test.title} - 失败: ${test.failures}/${test.runs} (${failRate}%)`);
});
console.log('');
}
}
analyzeSlowTests() {
const runs = this.trendData.runs;
const slowTestMap = new Map();
runs.forEach(run => {
run.slowestTests.forEach(test => {
const key = `${test.title}`;
if (!slowTestMap.has(key)) {
slowTestMap.set(key, {
title: test.title,
durations: [],
});
}
slowTestMap.get(key).durations.push(test.duration);
});
});
const slowTests = Array.from(slowTestMap.values())
.map(test => ({
title: test.title,
avgDuration: test.durations.reduce((a, b) => a + b, 0) / test.durations.length,
maxDuration: Math.max(...test.durations),
runs: test.durations.length,
}))
.sort((a, b) => b.avgDuration - a.avgDuration)
.slice(0, 10);
if (slowTests.length > 0) {
console.log('🐌 最慢的测试 (平均耗时):');
slowTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.title} - 平均: ${this.formatDuration(test.avgDuration)} - 最大: ${this.formatDuration(test.maxDuration)}`);
});
console.log('');
}
}
analyzeFailedTests() {
const runs = this.trendData.runs;
const failedTestMap = new Map();
runs.forEach(run => {
run.failedTests.forEach(test => {
const key = `${test.title}`;
if (!failedTestMap.has(key)) {
failedTestMap.set(key, {
title: test.title,
failures: 0,
lastFailure: null,
errorMessages: new Set(),
});
}
failedTestMap.get(key).failures++;
failedTestMap.get(key).lastFailure = run.timestamp;
if (test.error) {
failedTestMap.get(key).errorMessages.add(test.error);
}
});
});
const failedTests = Array.from(failedTestMap.values())
.sort((a, b) => b.failures - a.failures)
.slice(0, 10);
if (failedTests.length > 0) {
console.log('❌ 最常失败的测试:');
failedTests.forEach((test, index) => {
const lastFailure = new Date(test.lastFailure).toLocaleString('zh-CN');
console.log(` ${index + 1}. ${test.title} - 失败: ${test.failures}次 - 最后失败: ${lastFailure}`);
});
console.log('');
}
}
generateRecommendations() {
const summary = this.trendData.summary;
const runs = this.trendData.runs;
const recommendations = [];
if (summary.trend === 'degrading') {
recommendations.push('⚠️ 测试通过率呈下降趋势,建议检查最近的代码变更');
}
const recentFlakyRate = runs.slice(-5).reduce((sum, run) => sum + run.summary.flakyRate, 0) / 5;
if (recentFlakyRate > 10) {
recommendations.push('🔄 不稳定测试比例较高,建议优化测试稳定性');
}
const recentAvgDuration = runs.slice(-5).reduce((sum, run) => sum + run.summary.totalDuration, 0) / 5;
if (recentAvgDuration > 300000) {
recommendations.push('⏱️ 测试执行时间较长,建议优化测试性能');
}
if (recommendations.length > 0) {
console.log('💡 改进建议:');
recommendations.forEach(rec => {
console.log(` ${rec}`);
});
console.log('');
}
}
getTrendEmoji(trend) {
switch (trend) {
case 'improving':
return '📈';
case 'degrading':
return '📉';
default:
return '➡️';
}
}
formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
return `${(ms / 60000).toFixed(1)}m`;
}
}
}
// 命令行接口
if (require.main === module) {
const analyzer = new TestTrendAnalyzer();
const command = process.argv[2];
switch (command) {
case 'add':
const resultsFile = process.argv[3];
if (resultsFile && fs.existsSync(resultsFile)) {
const testResults = JSON.parse(fs.readFileSync(resultsFile, 'utf-8'));
analyzer.addTestRun(testResults);
console.log('✅ 测试数据已添加');
} else {
console.error('❌ 错误: 请提供有效的测试结果文件');
process.exit(1);
}
break;
case 'report':
analyzer.generateTrendReport();
break;
case 'export':
const exportFile = process.argv[3] || 'test-trends.json';
fs.writeFileSync(exportFile, JSON.stringify(analyzer.trendData, null, 2), 'utf-8');
console.log(`✅ 趋势数据已导出到: ${exportFile}`);
break;
default:
console.log('测试趋势分析工具');
console.log('');
console.log('用法:');
console.log(' node testTrendAnalyzer.js add <results.json> - 添加测试结果');
console.log(' node testTrendAnalyzer.js report - 生成趋势报告');
console.log(' node testTrendAnalyzer.js export [file.json] - 导出趋势数据');
console.log('');
break;
}
}
module.exports = TestTrendAnalyzer;
@@ -0,0 +1,348 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';
import { TestDataManager } from './utils/testDataManager';
import { TestHelper } from './utils/testHelper';
test.describe('用户管理异常场景测试', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let userManagementPage: UserManagementPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
userManagementPage = new UserManagementPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
});
test('创建用户 - 重复用户名', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试创建重复用户名的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const userData = {
username: 'admin',
nickname: '重复用户',
email: 'duplicate@example.com',
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
});
await test.step('验证错误消息', async () => {
await TestHelper.waitForErrorMessage(page);
const errorMessage = await TestHelper.getElementText(page, '.el-message__content');
expect(errorMessage).toContain('用户名已存在');
});
});
test('创建用户 - 无效邮箱格式', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试创建无效邮箱的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const userData = {
username: `testuser_${Date.now()}`,
nickname: '测试用户',
email: 'invalid-email',
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
});
await test.step('验证表单验证错误', async () => {
const emailInput = page.locator('input[name="email"]');
const hasError = await emailInput.evaluate(el => el.classList.contains('is-error'));
expect(hasError).toBeTruthy();
});
});
test('创建用户 - 密码强度不足', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试创建密码强度不足的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const userData = {
username: `testuser_${Date.now()}`,
nickname: '测试用户',
email: 'test@example.com',
phone: '13800138000',
password: '123',
confirmPassword: '123',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
});
await test.step('验证密码强度错误', async () => {
const passwordInput = page.locator('input[name="password"]');
const hasError = await passwordInput.evaluate(el => el.classList.contains('is-error'));
expect(hasError).toBeTruthy();
});
});
test('创建用户 - 密码不匹配', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试创建密码不匹配的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const userData = {
username: `testuser_${Date.now()}`,
nickname: '测试用户',
email: 'test@example.com',
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'DifferentPassword',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
});
await test.step('验证密码不匹配错误', async () => {
const confirmPasswordInput = page.locator('input[name="confirmPassword"]');
const hasError = await confirmPasswordInput.evaluate(el => el.classList.contains('is-error'));
expect(hasError).toBeTruthy();
});
});
test('创建用户 - 缺少必填字段', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试创建缺少必填字段的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const userData = {
username: '',
nickname: '',
email: '',
phone: '',
password: '',
confirmPassword: '',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
});
await test.step('验证必填字段验证', async () => {
const submitButton = page.locator('.el-dialog__footer button[type="submit"]');
const isDisabled = await submitButton.evaluate(el => el.disabled);
expect(isDisabled).toBeTruthy();
});
});
test('创建用户 - 无效手机号格式', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试创建无效手机号的用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const userData = {
username: `testuser_${Date.now()}`,
nickname: '测试用户',
email: 'test@example.com',
phone: '123',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
});
await test.step('验证手机号格式错误', async () => {
const phoneInput = page.locator('input[name="phone"]');
const hasError = await phoneInput.evaluate(el => el.classList.contains('is-error'));
expect(hasError).toBeTruthy();
});
});
test('编辑用户 - 不存在的用户ID', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试编辑不存在的用户', async () => {
await page.goto('/users/999999/edit');
await TestHelper.waitForPageLoad(page);
});
await test.step('验证404错误或重定向', async () => {
const currentUrl = page.url();
expect(currentUrl).toMatch(/(404|users)/);
});
});
test('删除用户 - 不存在的用户ID', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试删除不存在的用户', async () => {
const response = await page.request.delete('http://localhost:8084/api/users/999999');
expect(response.status()).toBe(404);
});
});
test('搜索用户 - 空搜索条件', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('执行空搜索', async () => {
await userManagementPage.search('');
await TestHelper.waitForPageLoad(page);
});
await test.step('验证显示所有用户', async () => {
const userCount = await userManagementPage.getUserCount();
expect(userCount).toBeGreaterThan(0);
});
});
test('搜索用户 - 不存在的用户名', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('搜索不存在的用户', async () => {
await userManagementPage.search('nonexistentuser123456');
await TestHelper.waitForPageLoad(page);
});
await test.step('验证无结果', async () => {
const userCount = await userManagementPage.getUserCount();
expect(userCount).toBe(0);
});
});
test('批量删除 - 未选择用户', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试批量删除未选择的用户', async () => {
await page.click('button:has-text("批量删除")');
});
await test.step('验证提示消息', async () => {
await TestHelper.waitForErrorMessage(page);
});
});
test('导出用户 - 无数据', async ({ page, request }) => {
await test.step('清空用户数据', async () => {
const response = await request.delete('http://localhost:8084/api/users/test/cleanup');
expect(response.ok()).toBeTruthy();
});
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试导出空数据', async () => {
const downloadPromise = page.waitForEvent('download');
await page.click('button:has-text("导出")');
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/users.*\.xlsx/);
});
});
test('分页 - 超出范围页码', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('尝试访问超出范围的页码', async () => {
await page.goto('/users?page=999999');
await TestHelper.waitForPageLoad(page);
});
await test.step('验证显示最后一页或第一页', async () => {
const currentPage = await userManagementPage.getCurrentPage();
expect(currentPage).toBeTruthy();
});
});
test('网络错误 - 创建用户时断网', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('模拟网络错误', async () => {
await page.route('**/api/users', route => route.abort('failed'));
});
await test.step('尝试创建用户', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
const userData = {
username: `testuser_${Date.now()}`,
nickname: '测试用户',
email: 'test@example.com',
phone: '13800138000',
password: 'Test123!@#',
confirmPassword: 'Test123!@#',
};
await userManagementPage.fillUserForm(userData);
await userManagementPage.submitForm();
});
await test.step('验证网络错误提示', async () => {
await TestHelper.waitForErrorMessage(page);
});
});
});
@@ -0,0 +1,243 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';
import { TestDataManager } from './utils/testDataManager';
import { TestHelper } from './utils/testHelper';
test.describe('用户管理 E2E 测试(改进版)', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let userManagementPage: UserManagementPage;
let testUser: any;
test.beforeAll(async ({ request }) => {
TestDataManager.initialize();
});
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
userManagementPage = new UserManagementPage(page);
await loginPage.goto();
await loginPage.login('admin', 'admin123');
});
test.afterEach(async ({ page, request }) => {
await TestHelper.clearAllStorage(page);
if (testUser) {
await TestDataManager.deleteTestUser(request, testUser.username);
testUser = null;
}
});
test('创建用户完整流程', async ({ page, request }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('点击创建用户按钮', async () => {
await userManagementPage.clickCreateUser();
await TestHelper.waitForElementVisible(page, '.el-dialog');
});
await test.step('生成测试用户数据', async () => {
testUser = TestDataManager.generateTestUser();
console.log('Generated test user:', testUser);
});
await test.step('填写用户表单', async () => {
await userManagementPage.fillUserForm(testUser);
});
await test.step('提交表单', async () => {
await userManagementPage.submitForm();
await TestHelper.waitForSuccessMessage(page);
});
await test.step('验证用户创建成功', async () => {
await TestHelper.waitForTextContent(page, '.el-table', testUser.username);
const userCount = await userManagementPage.getUserCount();
expect(userCount).toBeGreaterThan(0);
});
await test.step('通过API验证用户存在', async () => {
const response = await request.get(`http://localhost:8084/api/users?username=${testUser.username}`);
expect(response.ok()).toBeTruthy();
const result = await response.json();
expect(result.data).toBeDefined();
});
});
test('编辑用户流程', async ({ page, request }) => {
await test.step('创建测试用户', async () => {
testUser = TestDataManager.generateTestUser();
await TestDataManager.createTestUser(request, testUser);
});
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('搜索并编辑用户', async () => {
await userManagementPage.search(testUser.username);
await TestHelper.waitForTextContent(page, '.el-table', testUser.username);
await userManagementPage.editUser(1);
await TestHelper.waitForElementVisible(page, '.el-dialog');
});
await test.step('修改用户邮箱', async () => {
const newEmail = `updated_${testUser.email}`;
await page.fill('input[name="email"]', newEmail);
await userManagementPage.submitForm();
await TestHelper.waitForSuccessMessage(page);
});
await test.step('验证修改成功', async () => {
await TestHelper.waitForTextContent(page, '.el-table', 'updated_');
});
});
test('删除用户流程', async ({ page, request }) => {
await test.step('创建测试用户', async () => {
testUser = TestDataManager.generateTestUser();
await TestDataManager.createTestUser(request, testUser);
});
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('搜索并删除用户', async () => {
await userManagementPage.search(testUser.username);
await TestHelper.waitForTextContent(page, '.el-table', testUser.username);
await userManagementPage.deleteUser(1);
await TestHelper.waitForElementVisible(page, '.el-message-box');
});
await test.step('确认删除', async () => {
await userManagementPage.confirmDelete();
await TestHelper.waitForSuccessMessage(page);
});
await test.step('验证用户已删除', async () => {
await page.reload();
await TestHelper.waitForPageLoad(page);
await userManagementPage.search(testUser.username);
const userCount = await userManagementPage.getUserCount();
expect(userCount).toBe(0);
});
});
test('搜索用户功能', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('搜索admin用户', async () => {
await userManagementPage.search('admin');
await TestHelper.waitForTextContent(page, '.el-table', 'admin');
});
await test.step('验证搜索结果', async () => {
const userCount = await userManagementPage.getUserCount();
expect(userCount).toBeGreaterThan(0);
});
});
test('分页功能', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('获取当前页码', async () => {
const currentPage = await userManagementPage.getCurrentPage();
expect(currentPage).toBe('1');
});
await test.step('点击下一页', async () => {
await userManagementPage.nextPage();
await TestHelper.waitForPageLoad(page);
});
await test.step('验证页码变化', async () => {
const newPage = await userManagementPage.getCurrentPage();
expect(newPage).toBe('2');
});
});
test('批量删除用户', async ({ page, request }) => {
await test.step('创建多个测试用户', async () => {
const users = [];
for (let i = 0; i < 3; i++) {
const user = TestDataManager.generateTestUser();
await TestDataManager.createTestUser(request, user);
users.push(user);
}
});
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('选择多个用户', async () => {
await page.check('table tbody tr:nth-child(1) input[type="checkbox"]');
await page.check('table tbody tr:nth-child(2) input[type="checkbox"]');
});
await test.step('点击批量删除', async () => {
await page.click('button:has-text("批量删除")');
await TestHelper.waitForElementVisible(page, '.el-message-box');
});
await test.step('确认删除', async () => {
await page.click('.el-message-box__btns .el-button--primary');
await TestHelper.waitForSuccessMessage(page);
});
});
test('用户状态切换', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('切换用户状态', async () => {
await page.click('table tbody tr:first-child .status-toggle');
await TestHelper.waitForSuccessMessage(page);
});
await test.step('验证状态变化', async () => {
await page.reload();
await TestHelper.waitForPageLoad(page);
const statusElement = page.locator('table tbody tr:first-child .status-badge');
await TestHelper.waitForElementVisible(page, '.status-badge');
});
});
test('导出用户数据', async ({ page }) => {
await test.step('导航到用户管理页面', async () => {
await dashboardPage.navigateToUserManagement();
await TestHelper.waitForPageLoad(page);
});
await test.step('点击导出按钮', async () => {
const downloadPromise = page.waitForEvent('download');
await page.click('button:has-text("导出")');
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/users.*\.xlsx/);
});
});
});
@@ -0,0 +1,181 @@
import { APIRequestContext } from '@playwright/test';
export interface TestUser {
username: string;
nickname?: string;
email: string;
phone: string;
password: string;
roleIds?: number[];
}
export interface TestRole {
roleName: string;
roleKey: string;
roleSort: string;
status: string;
remark?: string;
}
export class TestDataManager {
private static testData: Map<string, any> = new Map();
private static apiBaseUrl: string;
static initialize(apiBaseUrl: string = 'http://localhost:8084') {
this.apiBaseUrl = apiBaseUrl;
}
static generateTimestamp(): string {
return Date.now().toString();
}
static generateTestUser(override?: Partial<TestUser>): TestUser {
const timestamp = this.generateTimestamp();
return {
username: `testuser_${timestamp}`,
nickname: `测试用户${timestamp}`,
email: `test_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
roleIds: [],
...override,
};
}
static generateTestRole(override?: Partial<TestRole>): TestRole {
const timestamp = this.generateTimestamp();
return {
roleName: `测试角色_${timestamp}`,
roleKey: `test_role_${timestamp}`,
roleSort: '1',
status: '1',
remark: `测试角色备注_${timestamp}`,
...override,
};
}
static async createTestUser(request: APIRequestContext, userData: TestUser): Promise<any> {
const response = await request.post(`${this.apiBaseUrl}/api/users`, {
data: userData,
});
if (!response.ok()) {
throw new Error(`Failed to create test user: ${await response.text()}`);
}
const result = await response.json();
const userId = result.data?.id || result.id;
this.testData.set(`user_${userData.username}`, {
id: userId,
...userData,
});
return result;
}
static async createTestRole(request: APIRequestContext, roleData: TestRole): Promise<any> {
const response = await request.post(`${this.apiBaseUrl}/api/roles`, {
data: roleData,
});
if (!response.ok()) {
throw new Error(`Failed to create test role: ${await response.text()}`);
}
const result = await response.json();
const roleId = result.data?.id || result.id;
this.testData.set(`role_${roleData.roleKey}`, {
id: roleId,
...roleData,
});
return result;
}
static async deleteTestUser(request: APIRequestContext, username: string): Promise<void> {
const userData = this.testData.get(`user_${username}`);
if (!userData || !userData.id) {
return;
}
const response = await request.delete(`${this.apiBaseUrl}/api/users/${userData.id}`);
if (!response.ok()) {
console.warn(`Failed to delete test user ${username}: ${await response.text()}`);
}
this.testData.delete(`user_${username}`);
}
static async deleteTestRole(request: APIRequestContext, roleKey: string): Promise<void> {
const roleData = this.testData.get(`role_${roleKey}`);
if (!roleData || !roleData.id) {
return;
}
const response = await request.delete(`${this.apiBaseUrl}/api/roles/${roleData.id}`);
if (!response.ok()) {
console.warn(`Failed to delete test role ${roleKey}: ${await response.text()}`);
}
this.testData.delete(`role_${roleKey}`);
}
static async cleanupTestData(request: APIRequestContext): Promise<void> {
const cleanupPromises: Promise<void>[] = [];
const entries = Array.from(this.testData.entries());
for (const [key, data] of entries) {
if (key.startsWith('user_')) {
cleanupPromises.push(this.deleteTestUser(request, data.username));
} else if (key.startsWith('role_')) {
cleanupPromises.push(this.deleteTestRole(request, data.roleKey));
}
}
await Promise.allSettled(cleanupPromises);
this.testData.clear();
}
static getTestData(key: string): any {
return this.testData.get(key);
}
static getAllTestData(): Map<string, any> {
return new Map(this.testData);
}
static clearTestData(): void {
this.testData.clear();
}
}
export class DatabaseHelper {
private static apiBaseUrl: string;
static initialize(apiBaseUrl: string = 'http://localhost:8084') {
this.apiBaseUrl = apiBaseUrl;
}
static async resetDatabase(request: APIRequestContext): Promise<void> {
const response = await request.post(`${this.apiBaseUrl}/api/test/reset-database`);
if (!response.ok()) {
throw new Error(`Failed to reset database: ${await response.text()}`);
}
}
static async clearTestData(request: APIRequestContext): Promise<void> {
const response = await request.post(`${this.apiBaseUrl}/api/test/clear-test-data`);
if (!response.ok()) {
throw new Error(`Failed to clear test data: ${await response.text()}`);
}
}
static async seedTestData(request: APIRequestContext): Promise<void> {
const response = await request.post(`${this.apiBaseUrl}/api/test/seed-test-data`);
if (!response.ok()) {
throw new Error(`Failed to seed test data: ${await response.text()}`);
}
}
}
+263
View File
@@ -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 || '';
}
}