优化分批执行点击菜单
This commit is contained in:
parent
015f8abf32
commit
40863e3eca
@ -12,9 +12,7 @@
|
|||||||
"test:menu": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js",
|
"test:menu": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js",
|
||||||
"test:menu:ui": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js --ui",
|
"test:menu:ui": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js --ui",
|
||||||
"test:menu:debug": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js --debug",
|
"test:menu:debug": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js --debug",
|
||||||
"test:menu:clean": "cross-env NODE_ENV=dev rimraf test-data/* && npm run test:menu",
|
"test:menu:clean": "cross-env NODE_ENV=dev rimraf test-data/* && npm run test:menu"
|
||||||
"test:menu:collect": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js --grep \"收集菜单数据\"",
|
|
||||||
"test:menu:batch": "cross-env NODE_ENV=dev playwright test tests/e2e/menu.spec.js --grep \"批量测试菜单\""
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"playwright",
|
"playwright",
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
const {defineConfig, devices} = require('@playwright/test');
|
const {defineConfig, devices} = require('@playwright/test');
|
||||||
|
const testLifecycle = require('./src/hooks/testLifecycle');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://playwright.dev/docs/test-configuration
|
* @see https://playwright.dev/docs/test-configuration
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @type {import('@playwright/test').PlaywrightTestConfig}
|
* @type {import('@playwright/test').PlaywrightTestConfig}
|
||||||
*/
|
*/
|
||||||
const config = {
|
const config = {
|
||||||
testDir: './tests',
|
testDir: './tests',
|
||||||
/* 测试超时时间 */
|
/* 测试超时时间 */
|
||||||
timeout: parseInt(process.env.EXPECT_TIMEOUT),
|
timeout: parseInt(process.env.EXPECT_TIMEOUT || '30000'),
|
||||||
/* 每个测试的预期状态 */
|
/* 每个测试的预期状态 */
|
||||||
expect: {
|
expect: {
|
||||||
timeout: parseInt(process.env.EXPECT_TIMEOUT)
|
timeout: parseInt(process.env.EXPECT_TIMEOUT || '30000')
|
||||||
},
|
},
|
||||||
/* 测试运行并发数 */
|
/* 测试运行并发数 */
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
@ -30,19 +29,21 @@ const config = {
|
|||||||
/* 收集测试追踪信息 */
|
/* 收集测试追踪信息 */
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
/* 自动截图 */
|
/* 自动截图 */
|
||||||
screenshot: 'only-on-failure',
|
screenshot: {
|
||||||
|
mode: 'only-on-failure'
|
||||||
|
},
|
||||||
/* 录制视频 */
|
/* 录制视频 */
|
||||||
video: 'retain-on-failure',
|
video: 'retain-on-failure',
|
||||||
/* 浏览器配置 */
|
/* 浏览器配置 */
|
||||||
headless: process.env.BROWSER_HEADLESS === 'true',
|
headless: process.env.BROWSER_HEADLESS === 'true',
|
||||||
viewport: {
|
viewport: {
|
||||||
width: parseInt(process.env.VIEWPORT_WIDTH),
|
width: parseInt(process.env.VIEWPORT_WIDTH || '1920'),
|
||||||
height: parseInt(process.env.VIEWPORT_HEIGHT)
|
height: parseInt(process.env.VIEWPORT_HEIGHT || '1080')
|
||||||
},
|
},
|
||||||
/* 浏览器启动选项 */
|
/* 浏览器启动选项 */
|
||||||
launchOptions: {
|
launchOptions: {
|
||||||
slowMo: parseInt(process.env.BROWSER_SLOW_MO),
|
slowMo: parseInt(process.env.BROWSER_SLOW_MO || '50'),
|
||||||
timeout: parseInt(process.env.BROWSER_TIMEOUT)
|
timeout: parseInt(process.env.BROWSER_TIMEOUT || '30000')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -60,6 +61,10 @@ const config = {
|
|||||||
// port: 3000,
|
// port: 3000,
|
||||||
// reuseExistingServer: !process.env.CI,
|
// reuseExistingServer: !process.env.CI,
|
||||||
// },
|
// },
|
||||||
|
|
||||||
|
/* 全局设置和清理 */
|
||||||
|
globalSetup: './src/hooks/setup.js',
|
||||||
|
globalTeardown: './src/hooks/teardown.js'
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = defineConfig(config);
|
||||||
@ -13,14 +13,6 @@ class LongiTestController {
|
|||||||
return await menuDataService.fetchAndSaveMenuData();
|
return await menuDataService.fetchAndSaveMenuData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取测试进度信息
|
|
||||||
* @returns {Object} - 进度信息
|
|
||||||
*/
|
|
||||||
getTestProgress() {
|
|
||||||
return menuDataService.getTestProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行所有菜单的测试
|
* 执行所有菜单的测试
|
||||||
* @returns {Promise<Object>} - 测试结果
|
* @returns {Promise<Object>} - 测试结果
|
||||||
|
|||||||
11
src/hooks/setup.js
Normal file
11
src/hooks/setup.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const testLifecycle = require('./testLifecycle');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局设置
|
||||||
|
* 在所有测试开始前执行
|
||||||
|
*/
|
||||||
|
async function globalSetup() {
|
||||||
|
await testLifecycle.beforeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = globalSetup;
|
||||||
11
src/hooks/teardown.js
Normal file
11
src/hooks/teardown.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const testLifecycle = require('./testLifecycle');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局清理
|
||||||
|
* 在所有测试结束后执行
|
||||||
|
*/
|
||||||
|
async function globalTeardown() {
|
||||||
|
await testLifecycle.afterAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = globalTeardown;
|
||||||
119
src/hooks/testLifecycle.js
Normal file
119
src/hooks/testLifecycle.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
const performanceService = require('../services/PerformanceService');
|
||||||
|
const FileUtils = require('../utils/FileUtils');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试生命周期管理
|
||||||
|
* 负责管理测试的全局生命周期,包括:
|
||||||
|
* 1. 测试开始前的初始化工作
|
||||||
|
* 2. 测试结束后的清理和报告生成
|
||||||
|
* 3. 其他全局测试设置
|
||||||
|
*/
|
||||||
|
class TestLifecycle {
|
||||||
|
constructor() {
|
||||||
|
this.reportPath = process.env.PERFORMANCE_REPORT_PATH || './test-results/performance-report.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试开始前的初始化
|
||||||
|
* 在所有测试开始前执行,用于准备测试环境
|
||||||
|
*/
|
||||||
|
async beforeAll() {
|
||||||
|
// 确保测试结果目录存在
|
||||||
|
await FileUtils.ensureDirectoryExists('./test-results');
|
||||||
|
// 清空之前的性能数据
|
||||||
|
await performanceService.clearPerformanceData();
|
||||||
|
console.log('✨ 测试环境初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试结束后的处理
|
||||||
|
* 在所有测试结束后执行,用于生成报告和清理工作
|
||||||
|
*/
|
||||||
|
async afterAll() {
|
||||||
|
const performanceData = performanceService.getPerformanceData();
|
||||||
|
const report = this.generateReport(performanceData);
|
||||||
|
await this.saveReport(report);
|
||||||
|
console.log(`📊 测试报告已生成: ${this.reportPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成测试报告
|
||||||
|
* @param {Array} performanceData 性能数据数组
|
||||||
|
* @returns {Object} 测试报告对象
|
||||||
|
*/
|
||||||
|
generateReport(performanceData) {
|
||||||
|
// 按页面名称分组的统计
|
||||||
|
const pageStats = {};
|
||||||
|
let totalDuration = 0;
|
||||||
|
let totalSuccess = 0;
|
||||||
|
let totalFailure = 0;
|
||||||
|
|
||||||
|
// 统计每个页面的性能数据
|
||||||
|
performanceData.forEach(record => {
|
||||||
|
const { pageName, duration, success } = record;
|
||||||
|
|
||||||
|
if (!pageStats[pageName]) {
|
||||||
|
pageStats[pageName] = {
|
||||||
|
totalTests: 0,
|
||||||
|
successCount: 0,
|
||||||
|
failureCount: 0,
|
||||||
|
totalDuration: 0,
|
||||||
|
averageDuration: 0,
|
||||||
|
minDuration: Infinity,
|
||||||
|
maxDuration: 0,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = pageStats[pageName];
|
||||||
|
stats.totalTests++;
|
||||||
|
success ? stats.successCount++ : stats.failureCount++;
|
||||||
|
stats.totalDuration += duration;
|
||||||
|
stats.minDuration = Math.min(stats.minDuration, duration);
|
||||||
|
stats.maxDuration = Math.max(stats.maxDuration, duration);
|
||||||
|
stats.averageDuration = stats.totalDuration / stats.totalTests;
|
||||||
|
|
||||||
|
if (!success && record.errorMessage) {
|
||||||
|
stats.errors.push({
|
||||||
|
timestamp: record.timestamp,
|
||||||
|
error: record.errorMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新总体统计
|
||||||
|
totalDuration += duration;
|
||||||
|
success ? totalSuccess++ : totalFailure++;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成报告
|
||||||
|
return {
|
||||||
|
summary: {
|
||||||
|
totalTests: performanceData.length,
|
||||||
|
successCount: totalSuccess,
|
||||||
|
failureCount: totalFailure,
|
||||||
|
successRate: (totalSuccess / performanceData.length * 100).toFixed(2) + '%',
|
||||||
|
averageDuration: totalDuration / performanceData.length,
|
||||||
|
totalDuration: totalDuration
|
||||||
|
},
|
||||||
|
pageStats,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
rawData: performanceData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存测试报告
|
||||||
|
* @param {Object} report 测试报告对象
|
||||||
|
*/
|
||||||
|
async saveReport(report) {
|
||||||
|
try {
|
||||||
|
await FileUtils.writeJsonFile(this.reportPath, report);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存测试报告失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建单例实例
|
||||||
|
const testLifecycle = new TestLifecycle();
|
||||||
|
module.exports = testLifecycle;
|
||||||
@ -3,6 +3,8 @@
|
|||||||
* 提供所有页面共用的方法和属性
|
* 提供所有页面共用的方法和属性
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const performanceService = require('../services/PerformanceService');
|
||||||
|
|
||||||
class BasePage {
|
class BasePage {
|
||||||
/**
|
/**
|
||||||
* 创建页面对象
|
* 创建页面对象
|
||||||
@ -192,20 +194,28 @@ class BasePage {
|
|||||||
async waitForIBPPageLoadWithRetry(pageName) {
|
async waitForIBPPageLoadWithRetry(pageName) {
|
||||||
console.log(`等待页面 ${pageName} 数据加载...`);
|
console.log(`等待页面 ${pageName} 数据加载...`);
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const {maxRetries, retryInterval, stabilityDelay} = this.config.pageLoad;
|
const {maxRetries, retryInterval, stabilityDelay} = this.config.pageLoad;
|
||||||
|
let errorMessage = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (retryCount < maxRetries) {
|
while (retryCount < maxRetries) {
|
||||||
// 检查错误状态
|
// 检查错误状态
|
||||||
const hasError = await this.checkPageError(pageName);
|
const hasError = await this.checkPageError(pageName);
|
||||||
if (hasError) return false;
|
if (hasError) {
|
||||||
|
errorMessage = await this.getErrorMessage();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查加载状态
|
// 检查加载状态
|
||||||
const isLoading = await this.elementExistsBySelector(this.selectors.loadingMask);
|
const isLoading = await this.elementExistsBySelector(this.selectors.loadingMask);
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
await this.waitForTimeout(stabilityDelay);
|
await this.waitForTimeout(stabilityDelay);
|
||||||
console.log(`✅ 页面 ${pageName} 加载完成`);
|
console.log(`✅ 页面 ${pageName} 加载完成`);
|
||||||
|
|
||||||
|
// 记录成功的性能数据
|
||||||
|
await performanceService.recordSuccess(pageName, startTime, Date.now());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,9 +223,17 @@ class BasePage {
|
|||||||
await this.waitForTimeout(retryInterval);
|
await this.waitForTimeout(retryInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`页面加载超时: ${pageName}, 重试次数: ${maxRetries}`);
|
if (retryCount >= maxRetries) {
|
||||||
|
errorMessage = `页面加载超时: ${pageName}, 重试次数: ${maxRetries}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录失败的性能数据
|
||||||
|
await performanceService.recordFailure(pageName, startTime, Date.now(), errorMessage);
|
||||||
|
console.error(errorMessage);
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 记录异常的性能数据
|
||||||
|
await performanceService.recordError(pageName, startTime, Date.now(), error);
|
||||||
console.error(`页面加载出错: ${pageName}, 错误信息: ${error.message}`);
|
console.error(`页面加载出错: ${pageName}, 错误信息: ${error.message}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -242,6 +260,21 @@ class BasePage {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取错误信息
|
||||||
|
* @returns {Promise<string>} 错误信息
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async getErrorMessage() {
|
||||||
|
const errorSelectors = [this.selectors.errorBox, this.selectors.errorMessage];
|
||||||
|
for (const selector of errorSelectors) {
|
||||||
|
const elements = await this.page.locator(selector).all();
|
||||||
|
if (elements.length > 0) {
|
||||||
|
return await this.getTextByElement(elements[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前URL
|
* 获取当前URL
|
||||||
@ -251,7 +284,6 @@ class BasePage {
|
|||||||
return this.page.url();
|
return this.page.url();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 等待指定时间
|
* 等待指定时间
|
||||||
* @param {number} ms 等待时间(毫秒)
|
* @param {number} ms 等待时间(毫秒)
|
||||||
|
|||||||
@ -119,7 +119,7 @@ class LongiMainPage extends BasePage {
|
|||||||
* @returns {Promise<Array>} 菜单项数组
|
* @returns {Promise<Array>} 菜单项数组
|
||||||
*/
|
*/
|
||||||
async findMenuItems() {
|
async findMenuItems() {
|
||||||
console.log('开始查找菜单项...');
|
// console.log('开始查找菜单项...');
|
||||||
|
|
||||||
// 等待菜单加载完成
|
// 等待菜单加载完成
|
||||||
await this.page.waitForSelector(this.selectors.sideNav, {
|
await this.page.waitForSelector(this.selectors.sideNav, {
|
||||||
@ -128,7 +128,7 @@ class LongiMainPage extends BasePage {
|
|||||||
|
|
||||||
// 获取所有菜单项
|
// 获取所有菜单项
|
||||||
const items = await this.page.locator(this.selectors.menuItems).all();
|
const items = await this.page.locator(this.selectors.menuItems).all();
|
||||||
console.log(`找到 ${items.length} 个菜单项元素`);
|
// console.log(`找到 ${items.length} 个菜单项元素`);
|
||||||
const menuItems = [];
|
const menuItems = [];
|
||||||
|
|
||||||
// 处理每个菜单项
|
// 处理每个菜单项
|
||||||
@ -165,7 +165,7 @@ class LongiMainPage extends BasePage {
|
|||||||
// 综合判断是否有三级菜单
|
// 综合判断是否有三级菜单
|
||||||
const hasThirdMenu = isSubMenuTitle || hasSubMenuIndicator || hasThirdLevelIndicator;
|
const hasThirdMenu = isSubMenuTitle || hasSubMenuIndicator || hasThirdLevelIndicator;
|
||||||
|
|
||||||
console.log(`菜单项 "${text.trim()}" ${hasThirdMenu ? '有' : '没有'}三级菜单 (通过DOM结构判断)`);
|
// console.log(`菜单项 "${text.trim()}" ${hasThirdMenu ? '有' : '没有'}三级菜单 (通过DOM结构判断)`);
|
||||||
|
|
||||||
// 生成唯一标识符,结合索引和文本
|
// 生成唯一标识符,结合索引和文本
|
||||||
const uniqueId = `menu_${i}_${text.trim().replace(/\s+/g, '_')}`;
|
const uniqueId = `menu_${i}_${text.trim().replace(/\s+/g, '_')}`;
|
||||||
|
|||||||
@ -83,7 +83,9 @@ class LongiTestService {
|
|||||||
|
|
||||||
const mainPage = new LongiMainPage(page);
|
const mainPage = new LongiMainPage(page);
|
||||||
await mainPage.handleAllMenuClicks(menuBatch);
|
await mainPage.handleAllMenuClicks(menuBatch);
|
||||||
await this.updateTestProgress(menuBatch);
|
} catch (error) {
|
||||||
|
console.error('批次测试执行失败:', error);
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
if (browser) await browser.close();
|
if (browser) await browser.close();
|
||||||
}
|
}
|
||||||
@ -95,13 +97,29 @@ class LongiTestService {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async updateTestProgress(completedBatch) {
|
async updateTestProgress(completedBatch) {
|
||||||
|
if (!completedBatch?.length) return;
|
||||||
|
|
||||||
const progress = this.getProgress();
|
const progress = this.getProgress();
|
||||||
const newProgress = [...progress];
|
const newProgress = [...progress];
|
||||||
|
|
||||||
|
// 验证并更新进度
|
||||||
for (const menuItem of completedBatch) {
|
for (const menuItem of completedBatch) {
|
||||||
|
if (!menuItem?.id) {
|
||||||
|
console.warn('警告: 菜单项缺少ID', menuItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!newProgress.includes(menuItem.id)) {
|
if (!newProgress.includes(menuItem.id)) {
|
||||||
newProgress.push(menuItem.id);
|
newProgress.push(menuItem.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证新进度的有效性
|
||||||
|
const menuData = this.getMenuData();
|
||||||
|
if (newProgress.length > menuData.length) {
|
||||||
|
console.error('错误: 进度数超过总菜单数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.saveProgress(newProgress);
|
this.saveProgress(newProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,39 +240,89 @@ class LongiTestService {
|
|||||||
* @returns {Promise<Object>} - 测试结果
|
* @returns {Promise<Object>} - 测试结果
|
||||||
*/
|
*/
|
||||||
async runAllTests() {
|
async runAllTests() {
|
||||||
// 清理之前的进度
|
try {
|
||||||
this.saveProgress([]);
|
// 获取菜单数据并验证
|
||||||
let retryCount = 0;
|
const menuData = this.getMenuData();
|
||||||
|
if (!menuData?.length) {
|
||||||
|
throw new Error('没有可用的菜单数据,请先执行 fetchAndSaveMenuData');
|
||||||
|
}
|
||||||
|
|
||||||
while (true) {
|
// 清理之前的进度
|
||||||
try {
|
this.saveProgress([]);
|
||||||
const batch = this.getNextBatch();
|
|
||||||
if (!batch?.length) {
|
let retryCount = 0;
|
||||||
const progress = this.getTestProgress();
|
let lastProgress = null;
|
||||||
|
|
||||||
if (progress.completed < progress.total && retryCount < this.retryCount) {
|
while (true) {
|
||||||
console.log(`\n还有未完成的测试,尝试重试 (${retryCount + 1}/${this.retryCount})...`);
|
try {
|
||||||
retryCount++;
|
const batch = this.getNextBatch();
|
||||||
const delay = Math.min(this.batchInterval * Math.pow(2, retryCount), this.maxRetryDelay);
|
if (!batch?.length) {
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
const progress = this.getTestProgress();
|
||||||
continue;
|
|
||||||
|
// 验证进度数据的有效性
|
||||||
|
if (!this.isValidProgress(progress)) {
|
||||||
|
throw new Error('进度数据无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress.completed < progress.total && retryCount < this.retryCount) {
|
||||||
|
console.log(`\n还有未完成的测试,尝试重试 (${retryCount + 1}/${this.retryCount})...`);
|
||||||
|
retryCount++;
|
||||||
|
const delay = Math.min(this.batchInterval * Math.pow(2, retryCount), this.maxRetryDelay);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
return progress;
|
await this.runBatchTest(batch);
|
||||||
}
|
await this.updateTestProgress(batch);
|
||||||
|
|
||||||
await this.runBatchTest(batch);
|
// 验证进度是否正确更新
|
||||||
await new Promise(resolve => setTimeout(resolve, this.batchInterval));
|
const currentProgress = this.getTestProgress();
|
||||||
} catch (error) {
|
if (lastProgress && currentProgress.completed <= lastProgress.completed) {
|
||||||
console.error('测试执行失败:', error);
|
console.warn('警告: 进度可能未正确更新');
|
||||||
if (error.message.includes('登录失败')) {
|
}
|
||||||
throw error; // 登录失败直接终止
|
lastProgress = currentProgress;
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, this.batchInterval));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('测试执行失败:', error);
|
||||||
|
if (error.message.includes('登录失败')) {
|
||||||
|
throw error; // 登录失败直接终止
|
||||||
|
}
|
||||||
|
// 其他错误继续下一批次
|
||||||
}
|
}
|
||||||
// 其他错误继续下一批次
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('运行所有测试失败:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证进度数据的有效性
|
||||||
|
* @param {Object} progress - 进度数据
|
||||||
|
* @returns {boolean} - 是否有效
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
isValidProgress(progress) {
|
||||||
|
if (!progress || typeof progress !== 'object') return false;
|
||||||
|
|
||||||
|
const menuData = this.getMenuData();
|
||||||
|
if (!menuData?.length) return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof progress.total === 'number' &&
|
||||||
|
typeof progress.completed === 'number' &&
|
||||||
|
typeof progress.remaining === 'number' &&
|
||||||
|
progress.total === menuData.length &&
|
||||||
|
progress.completed >= 0 &&
|
||||||
|
progress.completed <= progress.total &&
|
||||||
|
progress.remaining === (progress.total - progress.completed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取测试进度信息
|
* 获取测试进度信息
|
||||||
* @returns {Object} - 进度信息,包含总数、已完成数和剩余数
|
* @returns {Object} - 进度信息,包含总数、已完成数和剩余数
|
||||||
@ -264,15 +332,22 @@ class LongiTestService {
|
|||||||
const menuData = this.getMenuData();
|
const menuData = this.getMenuData();
|
||||||
const progress = this.getProgress();
|
const progress = this.getProgress();
|
||||||
|
|
||||||
if (!menuData) {
|
if (!menuData?.length) {
|
||||||
|
console.warn('警告: 没有可用的菜单数据');
|
||||||
return {total: 0, completed: 0, remaining: 0};
|
return {total: 0, completed: 0, remaining: 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const completed = progress.length;
|
||||||
total: menuData.length,
|
const total = menuData.length;
|
||||||
completed: progress.length,
|
const remaining = total - completed;
|
||||||
remaining: menuData.length - progress.length
|
|
||||||
};
|
// 验证数据一致性
|
||||||
|
if (completed > total) {
|
||||||
|
console.error('错误: 完成数超过总数');
|
||||||
|
return {total, completed: total, remaining: 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {total, completed, remaining};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取测试进度失败:', error);
|
console.error('获取测试进度失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
108
src/services/PerformanceService.js
Normal file
108
src/services/PerformanceService.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
const FileUtils = require('../utils/FileUtils');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性能数据服务
|
||||||
|
* 负责记录和管理页面性能数据
|
||||||
|
*/
|
||||||
|
class PerformanceService {
|
||||||
|
constructor() {
|
||||||
|
this.performanceData = [];
|
||||||
|
this.performanceDataPath = process.env.PERFORMANCE_DATA_FILE_PATH || './test-results/performance-data.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录成功的页面加载性能数据
|
||||||
|
* @param {string} pageName 页面名称
|
||||||
|
* @param {number} startTime 开始时间
|
||||||
|
* @param {number} endTime 结束时间
|
||||||
|
*/
|
||||||
|
async recordSuccess(pageName, startTime, endTime) {
|
||||||
|
await this._recordPerformance({
|
||||||
|
pageName,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
duration: endTime - startTime,
|
||||||
|
success: true,
|
||||||
|
errorMessage: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录失败的页面加载性能数据
|
||||||
|
* @param {string} pageName 页面名称
|
||||||
|
* @param {number} startTime 开始时间
|
||||||
|
* @param {number} endTime 结束时间
|
||||||
|
* @param {string} errorMessage 错误信息
|
||||||
|
*/
|
||||||
|
async recordFailure(pageName, startTime, endTime, errorMessage) {
|
||||||
|
await this._recordPerformance({
|
||||||
|
pageName,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
duration: endTime - startTime,
|
||||||
|
success: false,
|
||||||
|
errorMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录异常的页面加载性能数据
|
||||||
|
* @param {string} pageName 页面名称
|
||||||
|
* @param {number} startTime 开始时间
|
||||||
|
* @param {number} endTime 结束时间
|
||||||
|
* @param {Error} error 错误对象
|
||||||
|
*/
|
||||||
|
async recordError(pageName, startTime, endTime, error) {
|
||||||
|
await this._recordPerformance({
|
||||||
|
pageName,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
duration: endTime - startTime,
|
||||||
|
success: false,
|
||||||
|
errorMessage: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部方法:记录性能数据
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _recordPerformance(data) {
|
||||||
|
this.performanceData.push({
|
||||||
|
...data,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
await this.savePerformanceData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存性能数据到文件
|
||||||
|
*/
|
||||||
|
async savePerformanceData() {
|
||||||
|
try {
|
||||||
|
await FileUtils.writeJsonFile(this.performanceDataPath, this.performanceData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存性能数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空性能数据
|
||||||
|
*/
|
||||||
|
async clearPerformanceData() {
|
||||||
|
this.performanceData = [];
|
||||||
|
await this.savePerformanceData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取性能数据
|
||||||
|
* @returns {Array} 性能数据数组
|
||||||
|
*/
|
||||||
|
getPerformanceData() {
|
||||||
|
return this.performanceData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建单例实例
|
||||||
|
const performanceService = new PerformanceService();
|
||||||
|
module.exports = performanceService;
|
||||||
@ -2,7 +2,7 @@
|
|||||||
* 文件操作工具类
|
* 文件操作工具类
|
||||||
* 提供文件读写、目录创建等常用操作
|
* 提供文件读写、目录创建等常用操作
|
||||||
*/
|
*/
|
||||||
const fs = require('fs');
|
const fs = require('fs').promises;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
class FileUtils {
|
class FileUtils {
|
||||||
@ -25,17 +25,72 @@ class FileUtils {
|
|||||||
/**
|
/**
|
||||||
* 确保目录存在,如果不存在则创建
|
* 确保目录存在,如果不存在则创建
|
||||||
* @param {string} dirPath 目录路径
|
* @param {string} dirPath 目录路径
|
||||||
* @returns {boolean} 操作是否成功
|
|
||||||
*/
|
*/
|
||||||
static ensureDirectoryExists(dirPath) {
|
static async ensureDirectoryExists(dirPath) {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(dirPath)) {
|
await fs.access(dirPath);
|
||||||
fs.mkdirSync(dirPath, {recursive: true});
|
} catch {
|
||||||
console.log(`目录已创建: ${dirPath}`);
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入JSON文件
|
||||||
|
* @param {string} filePath 文件路径
|
||||||
|
* @param {Object} data 要写入的数据
|
||||||
|
*/
|
||||||
|
static async writeJsonFile(filePath, data) {
|
||||||
|
try {
|
||||||
|
// 确保目录存在
|
||||||
|
await this.ensureDirectoryExists(path.dirname(filePath));
|
||||||
|
// 写入文件
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`创建目录失败: ${dirPath}`, error);
|
console.error(`写入JSON文件失败 [${filePath}]:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取JSON文件
|
||||||
|
* @param {string} filePath 文件路径
|
||||||
|
* @returns {Promise<Object>} 读取的数据
|
||||||
|
*/
|
||||||
|
static async readJsonFile(filePath) {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
return JSON.parse(content);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`读取JSON文件失败 [${filePath}]:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件
|
||||||
|
* @param {string} filePath 文件路径
|
||||||
|
*/
|
||||||
|
static async deleteFile(filePath) {
|
||||||
|
try {
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'ENOENT') { // 如果错误不是"文件不存在"
|
||||||
|
console.error(`删除文件失败 [${filePath}]:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否存在
|
||||||
|
* @param {string} filePath 文件路径
|
||||||
|
* @returns {Promise<boolean>} 文件是否存在
|
||||||
|
*/
|
||||||
|
static async fileExists(filePath) {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1,7 @@
|
|||||||
[]
|
[
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
]
|
||||||
@ -1,34 +1,37 @@
|
|||||||
// 加载环境变量
|
// 加载环境变量
|
||||||
require('../../config/env');
|
require('../../config/env');
|
||||||
|
|
||||||
const { test } = require('@playwright/test');
|
const {test} = require('@playwright/test');
|
||||||
const TestController = require('../../src/controllers/LongiTestController');
|
const TestController = require('../../src/controllers/LongiTestController');
|
||||||
|
|
||||||
test.describe('测试所有隆基需求计划是否可用', () => {
|
test.describe('IBP系统菜单可访问性测试', () => {
|
||||||
let controller;
|
let controller;
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
controller = new TestController();
|
controller = new TestController();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('获取最新菜单数据', async () => {
|
test('应能成功获取系统所有可测试的菜单项', async () => {
|
||||||
const menuData = await controller.fetchAndSaveMenuData();
|
const menuData = await controller.fetchAndSaveMenuData();
|
||||||
|
test.expect(menuData).toBeTruthy();
|
||||||
test.expect(menuData.length).toBeGreaterThan(0);
|
test.expect(menuData.length).toBeGreaterThan(0);
|
||||||
console.log(`✓ 成功收集 ${menuData.length} 个菜单项`);
|
console.log(`✓ 成功收集 ${menuData.length} 个菜单项`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('访问并点击所有页面', async () => {
|
test('应能成功访问所有菜单页面', async () => {
|
||||||
// 执行所有测试
|
// 执行所有测试并获取进度
|
||||||
await controller.runAllTests();
|
const progress = await controller.runAllTests();
|
||||||
|
|
||||||
// 验证测试结果
|
// 验证测试结果
|
||||||
const progress = controller.getTestProgress();
|
test.expect(progress).toBeTruthy();
|
||||||
|
test.expect(progress.total).toBeGreaterThan(0);
|
||||||
test.expect(progress.completed).toBe(progress.total);
|
test.expect(progress.completed).toBe(progress.total);
|
||||||
|
test.expect(progress.remaining).toBe(0);
|
||||||
|
|
||||||
// 输出测试统计
|
// 输出测试统计
|
||||||
console.log('\n测试完成!');
|
console.log('\n测试完成!');
|
||||||
console.log(`✓ 总计测试: ${progress.total} 个菜单`);
|
console.log(`总菜单数: ${progress.total}`);
|
||||||
console.log(`✓ 成功完成: ${progress.completed} 个菜单`);
|
console.log(`完成数量: ${progress.completed}`);
|
||||||
console.log(`✓ 成功率: ${((progress.completed / progress.total) * 100).toFixed(2)}%`);
|
console.log(`剩余数量: ${progress.remaining}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue
Block a user