优化分批执行点击菜单

This commit is contained in:
dengqichen 2025-03-07 13:36:56 +08:00
parent ff891e82b0
commit 0dfad65b57
7 changed files with 140 additions and 374 deletions

View File

@ -1,25 +1,24 @@
# 基础URL # 基础配置
BASE_URL=https://ibp-dev.longi.com BASE_URL=https://ibp-dev.longi.com
# 测试配置
TEST_DATA_DIR=test-data
TEST_BATCH_SIZE=5
TEST_RETRY_COUNT=3
TEST_BATCH_INTERVAL=1000
TEST_MAX_RETRY_DELAY=5000
# 超时配置 # 超时配置
MENU_TIME_OUT=30000 MENU_TIME_OUT=30000
EXPECT_TIMEOUT=30000
# 数据目录配置
TEST_DATA_DIR=test-data
MENU_DATA_FILE_PATH=test-data/menu-data.json
TEST_PROGRESS_FILE_PATH=test-data/test-progress.json
# 测试批次配置
TEST_BATCH_SIZE=5
TEST_RETRY_COUNT=3
TEST_BATCH_INTERVAL=2000
TEST_MAX_RETRY_DELAY=5000
# 浏览器配置 # 浏览器配置
BROWSER_HEADLESS=false BROWSER_HEADLESS=false
BROWSER_SLOW_MO=50 BROWSER_SLOW_MO=50
BROWSER_TIMEOUT=30000 BROWSER_TIMEOUT=30000
# 视窗配置
VIEWPORT_WIDTH=1920 VIEWPORT_WIDTH=1920
VIEWPORT_HEIGHT=1080 VIEWPORT_HEIGHT=1080
# 数据目录配置
MENU_DATA_FILE_PATH=test-data/menu-data.json
TEST_PROGRESS_FILE_PATH=test-data/test-progress.json

206
README.md
View File

@ -1,137 +1,155 @@
# Playwright 自动化测试项目 # Playwright E2E Testing Framework
基于 Playwright 的自动化测试框架,专门用于测试 Web 应用的菜单可访问性 本项目是基于Playwright的端到端测试框架用于自动化测试Web应用的功能和性能
## 项目 ## 项目
``` ```
playwright-project/ playwright/
├── tests/ # 测试相关文件 ├── src/
│ ├── e2e/ # 端到端测试用例 │ ├── controllers/ # 控制器层,负责测试流程控制
│ │ └── menu.spec.js # 菜单测试用例 │ ├── pages/ # 页面对象层,封装页面操作
│ ├── pages/ # Page Object Models │ └── services/ # 服务层,处理数据和业务逻辑
│ │ ├── BasePage.js # 基础页面类 ├── tests/
│ │ ├── LoginPage.js # 登录页面类 │ └── e2e/ # 端到端测试用例
│ │ └── MainPage.js # 主页面类 ├── .env.dev # 开发环境配置
│ └── utils/ # 测试工具 └── playwright.config.js # Playwright配置文件
│ └── FileUtils.js # 文件操作工具
├── src/ # 源代码
│ ├── services/ # 业务服务
│ │ └── MenuDataService.js # 菜单数据服务
│ └── controllers/ # 业务控制器
│ └── TestController.js # 测试控制器
├── config/ # 配置文件
│ └── env.js # 环境变量配置
├── playwright.config.js # Playwright 配置
└── package.json # 项目配置
``` ```
## 功能特性 ## 配置管理
- 🔄 批量测试菜单可访问性 项目采用分层的配置管理方式,确保配置的统一性和可维护性:
- 💾 自动保存和恢复测试进度
- 🎯 支持分批次执行测试
- 🔍 详细的测试报告
- 🛠 可配置的测试参数
## 环境要求 ### 1. 环境变量配置 (.env.dev)
- Node.js 14+ 环境变量配置文件包含所有可配置项,按功能分类:
- npm 6+
- Playwright 依赖项
## 安装 ```ini
# 基础配置
```bash BASE_URL=https://ibp-dev.longi.com
# 安装依赖
npm install
# 安装 Playwright 浏览器
npx playwright install
```
## 配置
`.env.dev` 文件中配置以下环境变量:
```bash
# 基础URL
BASE_URL=https://your-app-url.com
# 测试配置 # 测试配置
TEST_BATCH_SIZE=5 # 每批次测试的菜单数量 TEST_DATA_DIR=test-data
TEST_RETRY_COUNT=3 # 失败重试次数 TEST_BATCH_SIZE=5
TEST_BATCH_INTERVAL=2000 # 批次间隔时间(ms) TEST_RETRY_COUNT=3
MENU_TIME_OUT=30000 # 菜单加载超时时间 TEST_BATCH_INTERVAL=1000
TEST_MAX_RETRY_DELAY=5000
# 超时配置
MENU_TIME_OUT=30000
EXPECT_TIMEOUT=30000
# 浏览器配置
BROWSER_HEADLESS=false
BROWSER_SLOW_MO=50
BROWSER_TIMEOUT=30000
VIEWPORT_WIDTH=1920
VIEWPORT_HEIGHT=1080
# 数据目录配置 # 数据目录配置
TEST_DATA_DIR=test-data
MENU_DATA_FILE_PATH=test-data/menu-data.json MENU_DATA_FILE_PATH=test-data/menu-data.json
TEST_PROGRESS_FILE_PATH=test-data/test-progress.json TEST_PROGRESS_FILE_PATH=test-data/test-progress.json
``` ```
## 使用方法 ### 2. Playwright配置 (playwright.config.js)
### 运行测试 Playwright配置文件统一管理所有浏览器相关的配置
```bash - 浏览器设置headless模式、视窗大小等
# 运行菜单测试 - 测试超时设置
npm run test:menu - 并发和重试策略
- 测试报告配置
# 使用 UI 模式运行测试 ### 3. 测试控制器配置 (TestController)
npm run test:menu:ui
# 调试模式运行测试 测试控制器只管理测试流程相关的配置:
npm run test:menu:debug
# 清理数据并重新测试 - 批次大小
npm run test:menu:clean - 重试次数
``` - 批次间隔
- 最大重试延迟
### 查看报告 ## 主要功能
```bash 1. **菜单遍历测试**
npm run report - 自动收集菜单数据
``` - 批量执行菜单点击测试
- 支持断点续测
- 提供测试进度跟踪
## 测试流程 2. **智能重试机制**
- 支持失败重试
- 使用指数退避策略
- 可配置最大重试次数和延迟
1. **数据收集** 3. **灵活的配置系统**
- 登录系统 - 所有配置项可通过环境变量覆盖
- 收集菜单数据 - 提供合理的默认值
- 保存到文件 - 配置项分类清晰
2. **批量测试** ## 使用说明
- 分批加载菜单
- 逐个点击测试 1. **安装依赖**
- 记录测试进度 ```bash
npm install
```
2. **配置环境变量**
- 复制 `.env.dev``.env`
- 根据需要修改配置项
3. **运行测试**
```bash
npm test
```
4. **查看报告**
```bash
npm run show-report
```
## 最佳实践
1. **配置管理**
- 所有配置统一在 `.env` 文件中管理
- 不同环境使用不同的 `.env` 文件
- 避免在代码中硬编码配置值
2. **测试用例编写**
- 使用页面对象模式
- 保持测试用例独立性
- 合理使用断言和超时设置
3. **错误处理** 3. **错误处理**
- 自动重试失败项 - 实现合理的重试机制
- 记录错误信息 - 记录详细的错误日志
- 生成测试报告 - 提供清晰的错误信息
## 开发指南 ## 注意事项
### 添加新的测试用例 1. 确保环境变量配置正确
2. 注意浏览器配置只在 `playwright.config.js` 中管理
3. 测试用例应该是独立和可重复的
4. 定期检查和清理测试数据
1. 在 `tests/e2e` 目录下创建测试文件 ## 常见问题
2. 使用 Page Object Model 模式
3. 遵循现有的代码结构
### 修改页面对象 1. **测试运行失败**
- 检查网络连接
- 验证环境变量配置
- 查看错误日志
1. 在 `tests/pages` 目录下修改或添加页面类 2. **配置不生效**
2. 继承 `BasePage` - 确认环境变量文件位置正确
3. 实现必要的页面方法 - 检查配置项拼写
- 重启测试进程
## 贡献指南 ## 贡献指南
1. Fork 项目 1. Fork 项目
2. 创建特性分支 2. 创建特性分支
3. 提交更改 3. 提交变更
4. 推送到分支 4. 发起 Pull Request
5. 创建 Pull Request
## 许可证 ## 许可证

View File

@ -1,160 +0,0 @@
const { chromium } = require('@playwright/test');
const LongiMainPage = require('../tests/pages/LongiMainPage');
const LongiLoginPage = require('../tests/pages/LongiLoginPage');
const menuDataService = require('../services/MenuDataService');
class TestController {
constructor() {
// 从环境变量获取配置
this.batchSize = parseInt(process.env.TEST_BATCH_SIZE || '5', 10);
this.retryCount = parseInt(process.env.TEST_RETRY_COUNT || '3', 10);
this.batchInterval = parseInt(process.env.TEST_BATCH_INTERVAL || '2000', 10);
// 浏览器配置
this.browserConfig = {
headless: false, // 使用有头模式
args: ['--start-maximized'], // 最大化窗口
slowMo: 50 // 放慢操作速度,便于观察
};
console.log('测试配置:', {
batchSize: this.batchSize,
retryCount: this.retryCount,
batchInterval: this.batchInterval
});
}
/**
* 执行登录操作
* @param {import('@playwright/test').Page} page - Playwright页面对象
* @returns {Promise<boolean>} 登录是否成功
*/
async performLogin(page) {
const loginPage = new LongiLoginPage(page);
// 导航到登录页面
await loginPage.navigateToLoginPage();
// 点击登录按钮
const loginSuccess = await loginPage.clickLoginButton();
if (!loginSuccess) {
console.error('登录失败');
return false;
}
console.log('登录成功');
return true;
}
/**
* 收集菜单数据
* @returns {Promise<Array>} - 处理后的菜单数据
*/
async collectMenuData() {
console.log('开始收集菜单数据...');
const browser = await chromium.launch(this.browserConfig);
const page = await browser.newPage();
// 设置视窗大小
await page.setViewportSize({ width: 1920, height: 1080 });
const mainPage = new LongiMainPage(page);
try {
// 使用 LongiLoginPage 处理登录
const loginSuccess = await this.performLogin(page);
if (!loginSuccess) {
throw new Error('登录失败,无法收集菜单数据');
}
console.log('登录成功,正在获取菜单项...');
const menuItems = await mainPage.checkAndLoadMenuItems();
console.log(`成功获取 ${menuItems.length} 个菜单项`);
return menuDataService.saveMenuData(menuItems);
} finally {
await browser.close();
}
}
/**
* 执行一批菜单的测试
* @param {Array} menuBatch - 要测试的菜单数组
*/
async runBatchTest(menuBatch) {
console.log(`开始执行批次测试,包含 ${menuBatch.length} 个菜单项:`);
console.log(menuBatch.map(m => m.text).join(', '));
const browser = await chromium.launch(this.browserConfig);
const page = await browser.newPage();
// 设置视窗大小
await page.setViewportSize({ width: 1920, height: 1080 });
const mainPage = new LongiMainPage(page);
try {
// 使用 LongiLoginPage 处理登录
const loginSuccess = await this.performLogin(page);
if (!loginSuccess) {
throw new Error('登录失败,无法执行菜单测试');
}
console.log('登录成功,开始测试菜单项...');
// 使用 handleAllMenuClicks 方法处理所有菜单
await mainPage.handleAllMenuClicks(menuBatch);
// 更新进度
const progress = menuDataService.getProgress();
const newProgress = [...progress];
for (const menuItem of menuBatch) {
if (!newProgress.includes(menuItem.id)) {
newProgress.push(menuItem.id);
}
}
menuDataService.saveProgress(newProgress);
} finally {
await browser.close();
}
}
/**
* 获取下一批要测试的菜单
* @returns {Array|null} - 下一批要测试的菜单如果没有则返回null
*/
getNextBatch() {
const menuData = menuDataService.getMenuData();
const progress = menuDataService.getProgress();
if (!menuData) return null;
// 过滤出未测试的菜单
const remainingMenus = menuData.filter(menu => !progress.includes(menu.id));
if (remainingMenus.length === 0) return null;
// 返回下一批要测试的菜单
return remainingMenus.slice(0, this.batchSize);
}
/**
* 获取测试进度信息
* @returns {Object} - 进度信息
*/
getTestProgress() {
const menuData = menuDataService.getMenuData();
const progress = menuDataService.getProgress();
if (!menuData) {
return { total: 0, completed: 0, remaining: 0 };
}
return {
total: menuData.length,
completed: progress.length,
remaining: menuData.length - progress.length
};
}
}
module.exports = TestController;

View File

@ -1,82 +0,0 @@
const fs = require('fs');
const path = require('path');
/**
* 菜单数据服务
* 负责菜单数据的存储和检索
*/
class MenuDataService {
constructor() {
// 从环境变量获取路径配置
this.dataDir = process.env.TEST_DATA_DIR;
this.menuDataPath = process.env.MENU_DATA_FILE_PATH;
this.progressPath = process.env.TEST_PROGRESS_FILE_PATH;
// 确保数据目录存在
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true });
}
}
/**
* 保存菜单数据
* @param {Array} menuItems - 从页面获取的原始菜单项
* @returns {Array} - 处理后的菜单数据
*/
saveMenuData(menuItems) {
const menuData = menuItems.map((menu, index) => ({
id: index + 1,
text: menu.text,
path: menu.path || menu.text,
hasThirdMenu: menu.hasThirdMenu,
parentMenu: menu.parentMenu
}));
fs.writeFileSync(this.menuDataPath, JSON.stringify(menuData, null, 2));
return menuData;
}
/**
* 读取菜单数据
* @returns {Array|null} - 菜单数据数组如果文件不存在则返回null
*/
getMenuData() {
if (!fs.existsSync(this.menuDataPath)) {
return null;
}
return JSON.parse(fs.readFileSync(this.menuDataPath, 'utf8'));
}
/**
* 保存测试进度
* @param {Array} completedMenus - 已完成测试的菜单ID数组
*/
saveProgress(completedMenus) {
fs.writeFileSync(this.progressPath, JSON.stringify(completedMenus, null, 2));
}
/**
* 读取测试进度
* @returns {Array} - 已完成测试的菜单ID数组
*/
getProgress() {
if (!fs.existsSync(this.progressPath)) {
return [];
}
return JSON.parse(fs.readFileSync(this.progressPath, 'utf8'));
}
/**
* 清理所有数据文件
*/
clearAll() {
if (fs.existsSync(this.menuDataPath)) {
fs.unlinkSync(this.menuDataPath);
}
if (fs.existsSync(this.progressPath)) {
fs.unlinkSync(this.progressPath);
}
}
}
// 导出单例实例
module.exports = new MenuDataService();

View File

@ -10,10 +10,7 @@ module.exports = defineConfig({
timeout: 60 * 60 * 1000, // 1小时 timeout: 60 * 60 * 1000, // 1小时
/* 每个测试的预期状态 */ /* 每个测试的预期状态 */
expect: { expect: {
/** timeout: parseInt(process.env.EXPECT_TIMEOUT || '30000', 10)
* 断言超时时间
*/
timeout: 30000
}, },
/* 测试运行并发数 */ /* 测试运行并发数 */
fullyParallel: false, fullyParallel: false,
@ -34,8 +31,17 @@ module.exports = defineConfig({
screenshot: 'only-on-failure', screenshot: 'only-on-failure',
/* 录制视频 */ /* 录制视频 */
video: 'retain-on-failure', video: 'retain-on-failure',
headless: false, /* 浏览器配置 */
viewport: { width: 1920, height: 1080 }, headless: process.env.BROWSER_HEADLESS === 'true',
viewport: {
width: parseInt(process.env.VIEWPORT_WIDTH || '1920', 10),
height: parseInt(process.env.VIEWPORT_HEIGHT || '1080', 10)
},
/* 浏览器启动选项 */
launchOptions: {
slowMo: parseInt(process.env.BROWSER_SLOW_MO || '50', 10),
timeout: parseInt(process.env.BROWSER_TIMEOUT || '30000', 10)
}
}, },
/* 配置不同的浏览器项目 */ /* 配置不同的浏览器项目 */

View File

@ -17,25 +17,11 @@ class TestController {
* @private * @private
*/ */
initializeConfig() { initializeConfig() {
// 从环境变量获取配置 // 从环境变量获取测试相关配置
this.batchSize = parseInt(process.env.TEST_BATCH_SIZE || '5', 10); this.batchSize = parseInt(process.env.TEST_BATCH_SIZE || '5', 10);
this.retryCount = parseInt(process.env.TEST_RETRY_COUNT || '3', 10); this.retryCount = parseInt(process.env.TEST_RETRY_COUNT || '3', 10);
this.batchInterval = parseInt(process.env.TEST_BATCH_INTERVAL || '2000', 10); this.batchInterval = parseInt(process.env.TEST_BATCH_INTERVAL || '2000', 10);
this.maxRetryDelay = parseInt(process.env.TEST_MAX_RETRY_DELAY || '5000', 10); this.maxRetryDelay = parseInt(process.env.TEST_MAX_RETRY_DELAY || '5000', 10);
// 浏览器配置
this.browserConfig = {
headless: process.env.BROWSER_HEADLESS === 'true',
args: ['--start-maximized'],
slowMo: parseInt(process.env.BROWSER_SLOW_MO || '50', 10),
timeout: parseInt(process.env.BROWSER_TIMEOUT || '30000', 10)
};
// 视窗配置
this.viewportConfig = {
width: parseInt(process.env.VIEWPORT_WIDTH || '1920', 10),
height: parseInt(process.env.VIEWPORT_HEIGHT || '1080', 10)
};
} }
/** /**
@ -45,9 +31,8 @@ class TestController {
*/ */
async createBrowser() { async createBrowser() {
try { try {
const browser = await chromium.launch(this.browserConfig); const browser = await chromium.launch();
const page = await browser.newPage(); const page = await browser.newPage();
await page.setViewportSize(this.viewportConfig);
return { browser, page }; return { browser, page };
} catch (error) { } catch (error) {
console.error('创建浏览器实例失败:', error); console.error('创建浏览器实例失败:', error);