优化分批执行点击菜单
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-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-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-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": [
|
"keywords": [
|
||||||
"playwright",
|
"playwright",
|
||||||
|
|||||||
@ -5,9 +5,9 @@ const {defineConfig, devices} = require('@playwright/test');
|
|||||||
* @see https://playwright.dev/docs/test-configuration
|
* @see https://playwright.dev/docs/test-configuration
|
||||||
*/
|
*/
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
testDir: './tests',
|
testDir: './tests/e2e',
|
||||||
/* 测试超时时间 */
|
/* 测试超时时间 */
|
||||||
timeout: 300 * 1000,
|
timeout: 60 * 60 * 1000, // 1小时
|
||||||
/* 每个测试的预期状态 */
|
/* 每个测试的预期状态 */
|
||||||
expect: {
|
expect: {
|
||||||
/**
|
/**
|
||||||
@ -17,24 +17,25 @@ module.exports = defineConfig({
|
|||||||
},
|
},
|
||||||
/* 测试运行并发数 */
|
/* 测试运行并发数 */
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
/* 失败重试次数 */
|
/* 失败重试次数 */
|
||||||
retries: 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: 1,
|
||||||
/* 测试报告相关 */
|
/* 测试报告相关 */
|
||||||
reporter: [
|
reporter: 'html',
|
||||||
['html', {open: 'never'}],
|
|
||||||
['list']
|
|
||||||
],
|
|
||||||
/* 共享设置 */
|
/* 共享设置 */
|
||||||
use: {
|
use: {
|
||||||
/* 基础URL */
|
/* 基础URL */
|
||||||
// baseURL: 'http://localhost:3000',
|
baseURL: process.env.BASE_URL,
|
||||||
|
|
||||||
/* 收集测试追踪信息 */
|
/* 收集测试追踪信息 */
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
/* 自动截图 */
|
/* 自动截图 */
|
||||||
screenshot: 'only-on-failure',
|
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',
|
name: 'chromium',
|
||||||
use: {...devices['Desktop Chrome']},
|
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