抽取共用部分

This commit is contained in:
dengqichen 2025-03-06 10:03:46 +08:00
parent 54296d639a
commit 23020e7825
2 changed files with 224 additions and 125 deletions

View File

@ -11,23 +11,89 @@ class LongiMainPage extends BasePage {
*/
constructor(page) {
super(page);
this.initializeSelectors();
this.initializeConfig();
}
// 页面元素选择器
/**
* 初始化选择器
* @private
*/
initializeSelectors() {
this.selectors = {
// 侧边导航菜单 - 使用更精确的选择器
sideNav: '.ly-side-nav, .el-menu', // 主菜单
menuToggle: '.hamburger-container, .fold-btn, button.hamburger, .vab-content .toggle-icon', // 菜单展开/收起按钮,提供多个可能的选择器
menuContainer: '.el-scrollbar__view > .el-menu', // 菜单容器
menuItems: '.el-sub-menu__title, .el-menu-item', // 菜单项
menuItemText: '.titleSpan', // 菜单项文本
firstLevelIndicator: '.menuIcon', // 一级菜单指示器
thirdLevelMenu: '.el-popper.is-light.el-popover .menuTitle.canClick', // 三级菜单项
thirdLevelIndicator: '.el-icon-arrow-right', // 三级菜单指示器(箭头图标)
subMenuIndicator: '.el-sub-menu__icon-arrow' // 子菜单指示器
// 侧边导航菜单
sideNav: '.ly-side-nav, .el-menu',
menuToggle: '.hamburger-container, .fold-btn, button.hamburger, .vab-content .toggle-icon',
menuContainer: '.el-scrollbar__view > .el-menu',
menuItems: '.el-sub-menu__title, .el-menu-item',
menuItemText: '.titleSpan',
firstLevelIndicator: '.menuIcon',
thirdLevelMenu: '.el-popper.is-light.el-popover .menuTitle.canClick',
thirdLevelIndicator: '.el-icon-arrow-right',
subMenuIndicator: '.el-sub-menu__icon-arrow',
// Tab相关
tabContainer: '.workSpaceBaseTab .el-tabs__item',
activeTab: '.vab-tabs .el-tabs--card .el-tabs__item.is-active',
closeButton: '.el-icon.is-icon-close',
// 加载状态
loadingMask: '.el-loading-mask',
errorBox: '.el-message-box__message',
errorMessage: '.el-message--error',
// 临时元素
temporaryElements: '.el-loading-mask, .el-message, .el-message-box'
};
}
// 设置超时时间
this.timeout = 10000;
/**
* 初始化配置
* @private
*/
initializeConfig() {
this.config = {
timeout: 10000,
pageLoad: {
maxRetries: 30,
retryInterval: 500,
stabilityDelay: 1000
},
menuTimeout: parseInt(process.env.MENU_TIME_OUT || '30000', 10)
};
}
/**
* 等待指定时间
* @param {number} ms 等待时间毫秒
* @private
*/
async wait(ms) {
await this.page.waitForTimeout(ms);
}
/**
* 检查元素是否存在
* @param {string} selector 元素选择器
* @returns {Promise<boolean>} 是否存在
* @private
*/
async elementExists(selector) {
const count = await this.page.locator(selector).count();
return count > 0;
}
/**
* 获取元素文本
* @param {Object} element Playwright元素对象
* @returns {Promise<string>} 元素文本
* @private
*/
async getElementText(element) {
try {
const text = await element.textContent();
return text.trim();
} catch (error) {
console.error('获取元素文本失败:', error.message);
return '';
}
}
/**
@ -51,27 +117,16 @@ class LongiMainPage extends BasePage {
/**
* 点击展开菜单
* @returns {Promise<void>}
*/
async clickExpandMenu() {
try {
// 尝试查找菜单切换按钮
const toggleButton = this.page.locator(this.selectors.menuToggle).first();
// 检查按钮是否可见
const isVisible = await toggleButton.isVisible().catch(() => false);
if (!isVisible) {
console.log('菜单切换按钮不可见,尝试其他方法');
// 如果按钮不可见可以尝试其他方法比如键盘快捷键或直接修改DOM
return;
}
// 点击菜单切换按钮
await toggleButton.click();
console.log('已点击展开菜单');
} catch (error) {
console.error('点击展开菜单时出错:', error);
const toggleButton = this.page.locator(this.selectors.menuToggle).first();
if (!await this.waitForElement(this.selectors.menuToggle, { returnBoolean: true, firstOnly: true })) {
console.log('菜单切换按钮不可见,尝试其他方法');
return;
}
await toggleButton.click();
console.log('已点击展开菜单');
}
/**
@ -334,33 +389,6 @@ class LongiMainPage extends BasePage {
return menuItem;
}
/**
* 执行内存回收
* @param {string} context 执行内存回收的上下文信息用于日志
* @returns {Promise<void>}
* @private
*/
async cleanupMemory(context = '') {
try {
await this.page.evaluate(() => {
// 清理DOM中的临时元素
const cleanup = () => {
const elements = document.querySelectorAll('.el-loading-mask, .el-message, .el-message-box');
elements.forEach(el => el.remove());
// 如果浏览器支持手动垃圾回收,则执行
if (window.gc) {
window.gc();
}
};
cleanup();
});
console.log(`🧹 执行内存回收${context ? ` - ${context}` : ''}`);
} catch (error) {
console.warn('执行内存回收时出错:', error.message);
}
}
/**
* 获取三级菜单列表
* @param {Object} parentMenu 父级菜单对象
@ -432,25 +460,33 @@ class LongiMainPage extends BasePage {
* @param {Object} parentMenu 父级菜单可选
*/
async handleMenuClick(menuInfo, parentMenu = null) {
const menuPath = parentMenu ? `${parentMenu.text} > ${menuInfo.text}` : menuInfo.text;
const menuPath = this.getMenuPath(menuInfo, parentMenu);
console.log(`点击菜单: ${menuPath}`);
// 1. 点击菜单并等待页面加载
await menuInfo.element.click();
const isLoaded = await this.waitForPageLoadWithRetry(menuInfo);
if (!await this.safeClick(menuInfo.element, menuPath)) {
return;
}
if (!isLoaded) {
if (!await this.waitForPageLoadWithRetry(menuInfo)) {
console.warn(`页面加载失败: ${menuPath}`);
return;
}
// 2. 处理页面中的标签页
await this.handleAllTabs(menuInfo);
// 3. 关闭当前标签页
await this.closeActiveTab(menuPath);
}
/**
* 获取菜单路径
* @param {Object} menuInfo 菜单信息对象
* @param {Object} parentMenu 父级菜单可选
* @returns {string} 菜单路径
* @private
*/
getMenuPath(menuInfo, parentMenu = null) {
return parentMenu ? `${parentMenu.text} > ${menuInfo.text}` : menuInfo.text;
}
/**
* 处理单个TAB页
* @param {Object} tabInfo TAB页信息对象
@ -474,28 +510,17 @@ class LongiMainPage extends BasePage {
*/
async handleAllTabs(menu) {
try {
// 等待TAB容器加载
await this.page.waitForTimeout(1000);
await this.wait(1000);
// 使用更精确的选择器获取工作区的TAB页
const tabs = await this.page.locator('.workSpaceBaseTab .el-tabs__item').all();
const tabs = await this.page.locator(this.selectors.tabContainer).all();
if (tabs.length === 0) {
console.log(`📝 ${menu.text} 没有TAB页`);
return;
}
console.log(`📑 ${menu.text} 找到 ${tabs.length} 个TAB页`);
const tabInfos = await this.getTabInfos(tabs);
// 获取所有TAB页的完整信息
const tabInfos = await Promise.all(
tabs.map(async element => ({
text: (await element.textContent()).trim(),
isActive: await element.evaluate(el => el.classList.contains('is-active')),
element: element
}))
);
// 处理每个非激活的TAB页
for (const tabInfo of tabInfos) {
if (!tabInfo.isActive) {
await this.handleSingleTab(tabInfo, menu);
@ -508,6 +533,22 @@ class LongiMainPage extends BasePage {
}
}
/**
* 获取TAB页信息
* @param {Array<Object>} tabs TAB页元素数组
* @returns {Promise<Array>} TAB页信息数组
* @private
*/
async getTabInfos(tabs) {
return Promise.all(
tabs.map(async element => ({
text: await this.getElementText(element),
isActive: await element.evaluate(el => el.classList.contains('is-active')),
element: element
}))
);
}
/**
* 等待页面加载完成带重试机制
* @param {Object} menu 菜单对象
@ -515,61 +556,59 @@ class LongiMainPage extends BasePage {
* @returns {Promise<boolean>} 页面是否加载成功
*/
async waitForPageLoadWithRetry(menu, subMenuText = '') {
const pageName = subMenuText ? `${menu.text} > ${subMenuText}` : menu.text;
const pageName = this.getMenuPath(menu, { text: subMenuText });
console.log(`等待页面 ${pageName} 数据加载...`);
const config = {
maxRetries: 30,
retryInterval: 500,
stabilityDelay: 1000
};
let retryCount = 0;
const { maxRetries, retryInterval, stabilityDelay } = this.config.pageLoad;
try {
while (retryCount < config.maxRetries) {
// 检查页面状态
const selectors = {
loadingMask: '.el-loading-mask',
errorBox: '.el-message-box__message',
errorMessage: '.el-message--error'
};
while (retryCount < maxRetries) {
// 检查错误状态
for (const [key, selector] of Object.entries(selectors)) {
const elements = await this.page.locator(selector).all();
if (elements.length > 0 && key !== 'loadingMask') {
const errorText = await elements[0].textContent();
console.error(`页面加载出现错误: ${pageName}, 错误信息: ${errorText}`);
return false;
}
}
const hasError = await this.checkPageError(pageName);
if (hasError) return false;
// 检查加载状态
const loadingMasks = await this.page.locator(selectors.loadingMask).all();
if (loadingMasks.length === 0) {
// 加载完成,等待页面稳定
await this.page.waitForTimeout(config.stabilityDelay);
const isLoading = await this.elementExists(this.selectors.loadingMask);
if (!isLoading) {
await this.wait(stabilityDelay);
console.log(`✅ 页面 ${pageName} 加载完成`);
return true;
}
// 继续等待
retryCount++;
await this.page.waitForTimeout(config.retryInterval);
await this.wait(retryInterval);
}
// 超时处理
console.error(`页面加载超时: ${pageName}, 重试次数: ${config.maxRetries}`);
console.error(`页面加载超时: ${pageName}, 重试次数: ${maxRetries}`);
return false;
} catch (error) {
// 任何错误都记录并返回false
console.error(`页面加载出错: ${pageName}, 错误信息: ${error.message}`);
return false;
}
}
/**
* 检查页面是否有错误
* @param {string} pageName 页面名称
* @returns {Promise<boolean>} 是否有错误
* @private
*/
async checkPageError(pageName) {
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) {
const errorText = await this.getElementText(elements[0]);
console.error(`页面加载出现错误: ${pageName}, 错误信息: ${errorText}`);
return true;
}
}
return false;
}
/**
* 关闭当前活动的标签页
* @param {string} pageName 页面名称用于日志显示
@ -578,16 +617,13 @@ class LongiMainPage extends BasePage {
try {
console.log(`🗑️ 正在关闭页面 "${pageName}" 的tab...`);
const activeTab = this.page.locator('.vab-tabs .el-tabs--card .el-tabs__item.is-active');
const closeButton = activeTab.locator('.el-icon.is-icon-close');
const activeTab = this.page.locator(this.selectors.activeTab);
const closeButton = activeTab.locator(this.selectors.closeButton);
const hasActiveTab = await activeTab.count() > 0;
const hasCloseButton = await closeButton.count() > 0;
if (hasActiveTab && hasCloseButton) {
if (await this.canCloseTab(activeTab, closeButton)) {
await closeButton.waitFor({state: 'visible', timeout: 5000});
await closeButton.click();
await this.page.waitForTimeout(500);
await this.safeClick(closeButton, `${pageName}的关闭按钮`);
await this.wait(500);
} else {
console.log(`⚠️ [${pageName}] 没有找到可关闭的tab继续执行...`);
}
@ -595,6 +631,37 @@ class LongiMainPage extends BasePage {
console.error(`关闭标签页时出错 [${pageName}]:`, error.message);
}
}
/**
* 检查是否可以关闭标签页
* @param {Object} activeTab 活动标签页元素
* @param {Object} closeButton 关闭按钮元素
* @returns {Promise<boolean>} 是否可以关闭
* @private
*/
async canCloseTab(activeTab, closeButton) {
const hasActiveTab = await activeTab.count() > 0;
const hasCloseButton = await closeButton.count() > 0;
return hasActiveTab && hasCloseButton;
}
/**
* 执行内存回收
* @param {string} context 执行内存回收的上下文信息
*/
async cleanupMemory(context = '') {
try {
await this.page.evaluate((selector) => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => el.remove());
if (window.gc) window.gc();
}, this.selectors.temporaryElements);
console.log(`🧹 执行内存回收${context ? ` - ${context}` : ''}`);
} catch (error) {
console.warn('执行内存回收时出错:', error.message);
}
}
}
module.exports = LongiMainPage;

View File

@ -28,12 +28,28 @@ class BasePage {
* 等待元素可见
* @param {string} selector 元素选择器
* @param {Object} options 选项
* @returns {Promise<import('@playwright/test').Locator>} 元素定位器
* @param {boolean} [options.returnBoolean=false] 是否返回布尔值而不是元素
* @param {boolean} [options.firstOnly=false] 是否只获取第一个元素
* @returns {Promise<import('@playwright/test').Locator|boolean>} 元素定位器或是否可见
*/
async waitForElement(selector, options = {}) {
const element = this.page.locator(selector);
await element.waitFor({state: 'visible', timeout: options.timeout || this.timeout});
return element;
try {
const element = options.firstOnly ?
this.page.locator(selector).first() :
this.page.locator(selector);
await element.waitFor({
state: 'visible',
timeout: options.timeout || this.timeout
});
return options.returnBoolean ? true : element;
} catch (error) {
if (options.returnBoolean) {
return false;
}
throw error;
}
}
/**
@ -145,6 +161,22 @@ class BasePage {
const element = await this.waitForElement(selector);
return element.getAttribute(attributeName);
}
/**
* 安全点击元素
* @param {Object} element Playwright元素对象
* @param {string} description 元素描述用于日志
* @returns {Promise<boolean>} 是否点击成功
*/
async safeClick(element, description) {
try {
await element.click();
return true;
} catch (error) {
console.error(`点击${description}失败:`, error.message);
return false;
}
}
}
module.exports = BasePage;