优化分批执行点击菜单

This commit is contained in:
dengqichen 2025-03-07 09:54:56 +08:00
parent 6aa60dd29d
commit 15b08c65df
6 changed files with 418 additions and 27 deletions

138
README.md
View File

@ -0,0 +1,138 @@
# Playwright 自动化测试项目
基于 Playwright 的自动化测试框架,专门用于测试 Web 应用的菜单可访问性。
## 项目结构
```
playwright-project/
├── tests/ # 测试相关文件
│ ├── e2e/ # 端到端测试用例
│ │ └── menu.spec.js # 菜单测试用例
│ ├── pages/ # Page Object Models
│ │ ├── BasePage.js # 基础页面类
│ │ ├── LoginPage.js # 登录页面类
│ │ └── MainPage.js # 主页面类
│ └── utils/ # 测试工具
│ └── FileUtils.js # 文件操作工具
├── src/ # 源代码
│ ├── services/ # 业务服务
│ │ └── MenuDataService.js # 菜单数据服务
│ └── controllers/ # 业务控制器
│ └── TestController.js # 测试控制器
├── config/ # 配置文件
│ └── env.js # 环境变量配置
├── playwright.config.js # Playwright 配置
└── package.json # 项目配置
```
## 功能特性
- 🔄 批量测试菜单可访问性
- 💾 自动保存和恢复测试进度
- 🎯 支持分批次执行测试
- 🔍 详细的测试报告
- 🛠 可配置的测试参数
## 环境要求
- Node.js 14+
- npm 6+
- Playwright 依赖项
## 安装
```bash
# 安装依赖
npm install
# 安装 Playwright 浏览器
npx playwright install
```
## 配置
`.env.dev` 文件中配置以下环境变量:
```bash
# 基础URL
BASE_URL=https://your-app-url.com
# 测试配置
TEST_BATCH_SIZE=5 # 每批次测试的菜单数量
TEST_RETRY_COUNT=3 # 失败重试次数
TEST_BATCH_INTERVAL=2000 # 批次间隔时间(ms)
MENU_TIME_OUT=30000 # 菜单加载超时时间
# 数据目录配置
TEST_DATA_DIR=test-data
MENU_DATA_FILE_PATH=test-data/menu-data.json
TEST_PROGRESS_FILE_PATH=test-data/test-progress.json
```
## 使用方法
### 运行测试
```bash
# 运行菜单测试
npm run test:menu
# 使用 UI 模式运行测试
npm run test:menu:ui
# 调试模式运行测试
npm run test:menu:debug
# 清理数据并重新测试
npm run test:menu:clean
```
### 查看报告
```bash
npm run report
```
## 测试流程
1. **数据收集**
- 登录系统
- 收集菜单数据
- 保存到文件
2. **批量测试**
- 分批加载菜单
- 逐个点击测试
- 记录测试进度
3. **错误处理**
- 自动重试失败项
- 记录错误信息
- 生成测试报告
## 开发指南
### 添加新的测试用例
1. 在 `tests/e2e` 目录下创建测试文件
2. 使用 Page Object Model 模式
3. 遵循现有的代码结构
### 修改页面对象
1. 在 `tests/pages` 目录下修改或添加页面类
2. 继承 `BasePage`
3. 实现必要的页面方法
## 贡献指南
1. Fork 项目
2. 创建特性分支
3. 提交更改
4. 推送到分支
5. 创建 Pull Request
## 许可证
MIT

View File

@ -12,7 +12,11 @@
"test:longi:check-normal:dev": "cross-env NODE_ENV=dev node scripts/run-tests.js",
"test:longi:check-clean": "cross-env NODE_ENV=dev node scripts/run-tests.js --clean",
"test:longi:check-collect": "cross-env NODE_ENV=dev node scripts/run-tests.js --collect-only",
"test:longi:check-clean-continue": "cross-env NODE_ENV=dev node scripts/run-tests.js --clean --continue"
"test:longi:check-clean-continue": "cross-env NODE_ENV=dev node scripts/run-tests.js --clean --continue",
"test:menu": "cross-env NODE_ENV=dev playwright test menu.spec.js",
"test:menu:ui": "cross-env NODE_ENV=dev playwright test menu.spec.js --ui",
"test:menu:debug": "cross-env NODE_ENV=dev playwright test menu.spec.js --debug",
"test:menu:clean": "cross-env NODE_ENV=dev rimraf test-data/* && npm run test:menu"
},
"keywords": [
"playwright",

View File

@ -5,9 +5,9 @@ const {defineConfig, devices} = require('@playwright/test');
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
testDir: './tests',
testDir: './tests/e2e',
/* 测试超时时间 */
timeout: 300 * 1000,
timeout: 60 * 60 * 1000, // 1小时
/* 每个测试的预期状态 */
expect: {
/**
@ -17,24 +17,25 @@ module.exports = defineConfig({
},
/* 测试运行并发数 */
fullyParallel: false,
forbidOnly: !!process.env.CI,
/* 失败重试次数 */
retries: 0,
retries: process.env.CI ? 2 : 0,
workers: 1,
/* 测试报告相关 */
reporter: [
['html', {open: 'never'}],
['list']
],
reporter: 'html',
/* 共享设置 */
use: {
/* 基础URL */
// baseURL: 'http://localhost:3000',
baseURL: process.env.BASE_URL,
/* 收集测试追踪信息 */
trace: 'on-first-retry',
/* 自动截图 */
screenshot: 'only-on-failure',
/* 录制视频 */
video: 'on-first-retry',
video: 'retain-on-failure',
headless: false,
viewport: { width: 1920, height: 1080 },
},
/* 配置不同的浏览器项目 */
@ -43,23 +44,6 @@ module.exports = defineConfig({
name: 'chromium',
use: {...devices['Desktop Chrome']},
},
{
name: 'firefox',
use: {...devices['Desktop Firefox']},
},
{
name: 'webkit',
use: {...devices['Desktop Safari']},
},
/* 移动设备测试 */
{
name: 'Mobile Chrome',
use: {...devices['Pixel 5']},
},
{
name: 'Mobile Safari',
use: {...devices['iPhone 12']},
},
],
/* 本地开发服务器配置 */

View File

@ -0,0 +1,131 @@
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
};
}
/**
* 执行登录操作
* @param {import('@playwright/test').Page} page - Playwright页面对象
* @returns {Promise<boolean>} 登录是否成功
*/
async performLogin(page) {
const loginPage = new LongiLoginPage(page);
await loginPage.navigateToLoginPage();
return await loginPage.clickLoginButton();
}
/**
* 收集菜单数据
* @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 {
if (!await this.performLogin(page)) {
throw new Error('登录失败,无法收集菜单数据');
}
const menuItems = await mainPage.checkAndLoadMenuItems();
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 {
if (!await this.performLogin(page)) {
throw new Error('登录失败,无法执行菜单测试');
}
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

@ -0,0 +1,82 @@
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();

52
tests/e2e/menu.spec.js Normal file
View File

@ -0,0 +1,52 @@
const { test } = require('@playwright/test');
const TestController = require('../../src/controllers/TestController');
const menuDataService = require('../../src/services/MenuDataService');
test.describe('菜单可访问性测试', () => {
let controller;
test.beforeAll(async () => {
controller = new TestController();
});
test('收集菜单数据', async () => {
const menuData = await controller.collectMenuData();
console.log(`成功收集 ${menuData.length} 个菜单项`);
});
test('批量测试菜单', async () => {
// 清理之前的进度
menuDataService.saveProgress([]);
let retryCount = 0;
const maxRetries = parseInt(process.env.TEST_RETRY_COUNT || '3', 10);
while (true) {
const batch = controller.getNextBatch();
if (!batch || batch.length === 0) {
const finalProgress = controller.getTestProgress();
if (finalProgress.completed < finalProgress.total && retryCount < maxRetries) {
console.log(`\n还有未完成的测试,尝试重试 (${retryCount + 1}/${maxRetries})...`);
retryCount++;
continue;
}
console.log('\n所有测试完成');
console.log(`最终进度: ${finalProgress.completed}/${finalProgress.total}`);
break;
}
// 显示当前进度
const currentProgress = controller.getTestProgress();
console.log(`\n当前进度: ${currentProgress.completed}/${currentProgress.total}`);
// 执行当前批次
await controller.runBatchTest(batch);
// 批次间暂停
const batchInterval = parseInt(process.env.TEST_BATCH_INTERVAL || '2000', 10);
await new Promise(resolve => setTimeout(resolve, batchInterval));
}
});
});