优化分批执行点击菜单
This commit is contained in:
parent
6aa60dd29d
commit
15b08c65df
138
README.md
138
README.md
@ -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
|
||||
@ -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",
|
||||
|
||||
@ -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']},
|
||||
},
|
||||
],
|
||||
|
||||
/* 本地开发服务器配置 */
|
||||
|
||||
131
src/controllers/TestController.js
Normal file
131
src/controllers/TestController.js
Normal 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;
|
||||
82
src/services/MenuDataService.js
Normal file
82
src/services/MenuDataService.js
Normal 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
52
tests/e2e/menu.spec.js
Normal 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));
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user