抽取共用部分
This commit is contained in:
parent
54296d639a
commit
23020e7825
@ -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;
|
||||
@ -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;
|
||||
Loading…
Reference in New Issue
Block a user