playwright/tests/pages/LongiMainPage.js
2025-03-06 09:27:38 +08:00

647 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 龙蛟IBP系统主页面对象模型
*/
const BasePage = require('../utils/BasePage');
const FileUtils = require('../utils/FileUtils');
class LongiMainPage extends BasePage {
/**
* 创建主页面对象
* @param {import('@playwright/test').Page} page Playwright页面对象
*/
constructor(page) {
super(page);
// 页面元素选择器
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' // 子菜单指示器
};
// 设置超时时间
this.timeout = 10000;
}
/**
* 检查菜单是否已展开
* @returns {Promise<boolean>} 菜单是否已展开
*/
async isMenuExpanded() {
// 获取菜单元素
const sideNav = this.page.locator(this.selectors.sideNav).first();
// 检查菜单的位置
const leftValue = await sideNav.evaluate(el => {
const style = window.getComputedStyle(el);
console.log(`菜单现在的偏移量是:${style.left}`);
return style.left;
});
// 如果left是0px说明菜单已展开
return leftValue === '0px';
}
/**
* 点击展开菜单
* @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);
}
}
/**
* 检查并展开侧边菜单
* 如果菜单已收起,则点击展开
* @returns {Promise<boolean>} 是否执行了展开操作
*/
async expandSideMenu() {
try {
// 检查菜单是否已展开
const isExpanded = await this.isMenuExpanded();
if (!isExpanded) {
console.log('菜单未展开,点击展开');
await this.clickExpandMenu();
return true;
} else {
// console.log('菜单已经处于展开状态');
return false;
}
} catch (error) {
console.error('展开菜单时出错:', error);
return false;
}
}
/**
* 检查菜单数据文件是否存在并加载数据
*/
async checkAndLoadMenuItems() {
try {
// 加载JSON文件
const menuItems = FileUtils.loadFromJsonFile(process.env.MENU_DATA_FILE_PATH);
// 检查是否成功加载数据
if (menuItems && Array.isArray(menuItems) && menuItems.length > 0) {
console.log(`从文件 ${process.env.BASE_URL} 成功加载了 ${menuItems.length} 个菜单项`);
return menuItems;
} else {
await this.expandSideMenu();
return await this.findAndSaveMenuItems();
}
} catch (error) {
console.error(`检查并加载菜单项时出错: ${error}`);
return null;
}
}
/**
* 查找菜单项并保存到文件
*/
async findAndSaveMenuItems() {
try {
// 查找菜单项
const menuItems = await this.findMenuItems();
// 如果没有找到菜单项,则返回空数组
if (!menuItems || menuItems.length === 0) {
console.warn('未找到任何菜单项,无法保存到文件');
return [];
}
// 过滤掉不能序列化的element属性
const menuItemsForSave = menuItems.map(({element, ...rest}) => rest);
// 保存到文件
FileUtils.saveToJsonFile(menuItemsForSave, process.env.MENU_DATA_FILE_PATH);
console.log(`已找到并保存 ${menuItems.length} 个菜单项到文件: ${process.env.MENU_DATA_FILE_PATH}`);
return menuItems;
} catch (error) {
console.error('查找并保存菜单项时出错:', error);
return [];
}
}
/**
* 查找所有菜单项
* @returns {Promise<Array>} 菜单项数组
*/
async findMenuItems() {
try {
console.log('开始查找菜单项...');
// 等待菜单加载完成
await this.page.waitForSelector(this.selectors.sideNav, {
timeout: parseInt(process.env.MENU_TIME_OUT || '30000', 10)
});
// 获取所有菜单项
const items = await this.page.locator(this.selectors.menuItems).all();
console.log(`找到 ${items.length} 个菜单项元素`);
const menuItems = [];
// 处理每个菜单项
for (let i = 0; i < items.length; i++) {
const item = items[i];
//过滤一级菜单
let isTopMenu = (await item.locator(this.selectors.firstLevelIndicator).count()) > 0;
if (isTopMenu) {
continue;
}
// 获取菜单项文本
const text = await item.textContent();
// 检查是否是可见的菜单项
const isVisible = await item.isVisible();
if (isVisible && text.trim()) {
// 检查是否有子菜单指示器
const hasSubMenuIndicator = await item.evaluate(el => {
return el.querySelector('.el-submenu__icon-arrow') !== null ||
el.querySelector('.el-icon-arrow-down') !== null;
});
// 检查是否有三级菜单指示器
const hasThirdLevelIndicator = await item.evaluate(el => {
// 检查是否有特定的三级菜单指示器
return el.classList.contains('is-opened') ||
el.querySelector('.third-level-menu') !== null ||
el.querySelector('.el-menu--inline') !== null;
});
// 检查是否是子菜单标题
const isSubMenuTitle = await item.evaluate(el => el.classList.contains('el-sub-menu__title'));
// 综合判断是否有三级菜单
const hasThirdMenu = isSubMenuTitle || hasSubMenuIndicator || hasThirdLevelIndicator;
console.log(`菜单项 "${text.trim()}" ${hasThirdMenu ? '有' : '没有'}三级菜单 (通过DOM结构判断)`);
// 生成唯一标识符,结合索引和文本
const uniqueId = `menu_${i}_${text.trim().replace(/\s+/g, '_')}`;
// 获取菜单路径
const menuPath = await this.getMenuPath(item);
menuItems.push({
index: i,
text: text.trim(),
element: item,
hasThirdMenu: hasThirdMenu,
uniqueId: uniqueId,
// 添加路径信息,帮助识别菜单层级
path: menuPath
});
}
}
console.log(`🔍 找到 ${menuItems.length} 个可测试的菜单项`);
return menuItems;
} catch (error) {
console.error('查找菜单项时出错:', error);
return [];
}
}
/**
* 获取菜单项的路径信息
* @param {Object} menuItem 菜单项元素
* @returns {Promise<string>} 菜单路径
*/
async getMenuPath(menuItem) {
try {
// 尝试获取父级菜单的文本
const parentText = await menuItem.evaluate(el => {
// 查找最近的父级菜单项
const parent = el.closest('.el-submenu');
if (parent) {
const parentTitle = parent.querySelector('.el-submenu__title');
if (parentTitle) {
const titleSpan = parentTitle.querySelector('.titleSpan');
return titleSpan ? titleSpan.textContent.trim() : parentTitle.textContent.trim();
}
}
return '';
});
const itemText = await menuItem.textContent();
if (parentText) {
return `${parentText} > ${itemText}`;
}
return itemText;
} catch (error) {
console.error('获取菜单路径时出错:', error);
return '';
}
}
async handleAllMenuClicks(menuItems) {
for (let i = 0; i < menuItems.length; i++) {
await this.handleSingleMenuClick(menuItems[i]);
}
}
/**
* 恢复菜单项的element元素
* @param {Object} menuItem 需要恢复element的菜单项
* @returns {Promise<Object>} 返回更新后的菜单项
* @private
*/
async restoreMenuElement(menuItem) {
try {
// 获取当前页面上的所有菜单元素
const currentMenuElements = await this.page.locator(this.selectors.menuItems).all();
let foundElement = null;
let sameTextElements = [];
// 首先找到所有文本匹配的元素
for (const element of currentMenuElements) {
const elementText = await element.textContent();
if (elementText.trim() === menuItem.text) {
// 检查是否是一级菜单
const isTopMenu = await element.locator(this.selectors.firstLevelIndicator).count() > 0;
if (!isTopMenu) {
sameTextElements.push(element);
}
}
}
// 如果找到多个同名菜单,根据是否有三级菜单来区分
if (sameTextElements.length > 0) {
for (const element of sameTextElements) {
// 检查是否有三级菜单指示器
const hasThirdMenu = await element.evaluate(el => {
return el.classList.contains('el-sub-menu__title') ||
el.querySelector('.el-submenu__icon-arrow') !== null;
});
if (hasThirdMenu === menuItem.hasThirdMenu) {
foundElement = element;
break;
}
}
// 如果还没找到,就用第一个匹配的元素
if (!foundElement && sameTextElements.length > 0) {
foundElement = sameTextElements[0];
}
}
if (foundElement) {
menuItem.element = foundElement;
console.log(`✅ 成功恢复菜单项 "${menuItem.text}" (${menuItem.hasThirdMenu ? '有' : '无'}三级菜单) 的element元素`);
} else {
console.warn(`⚠️ 无法恢复菜单项 "${menuItem.text}" (${menuItem.hasThirdMenu ? '有' : '无'}三级菜单) 的element元素`);
}
return menuItem;
} catch (error) {
console.error(`恢复菜单项element时出错 [${menuItem.text}]:`, error.message);
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);
}
}
async handleSingleMenuClick(menu) {
try {
await this.expandSideMenu();
// 在处理菜单前重新获取最新的element
menu = await this.restoreMenuElement(menu);
if (!menu.element) {
console.error(`无法找到菜单项 "${menu.text}" 的element跳过处理`);
return;
}
if (menu.hasThirdMenu) {
await this.handleThreeLevelMenu(menu);
} else {
// 处理二级菜单点击
await this.handleMenuClick(menu);
}
// 执行内存回收
await this.cleanupMemory(menu.text);
} catch (error) {
console.error(`处理菜单失败 [${menu.text}]:`, error.message);
throw error;
}
}
/**
* 等待页面加载完成,带重试机制
* @param {Object} menu 菜单对象
* @param {string} subMenuText 子菜单文本(可选)
* @returns {Promise<boolean>} 页面是否加载成功
*/
async waitForPageLoadWithRetry(menu, subMenuText = '') {
const pageName = subMenuText ? `${menu.text} > ${subMenuText}` : menu.text;
console.log(`等待页面 ${pageName} 数据加载...`);
const config = {
maxRetries: 30,
retryInterval: 500,
stabilityDelay: 1000
};
let retryCount = 0;
try {
while (retryCount < config.maxRetries) {
// 检查页面状态
const selectors = {
loadingMask: '.el-loading-mask',
errorBox: '.el-message-box__message',
errorMessage: '.el-message--error'
};
// 检查错误状态
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 loadingMasks = await this.page.locator(selectors.loadingMask).all();
if (loadingMasks.length === 0) {
// 加载完成,等待页面稳定
await this.page.waitForTimeout(config.stabilityDelay);
console.log(`✅ 页面 ${pageName} 加载完成`);
return true;
}
// 继续等待
retryCount++;
await this.page.waitForTimeout(config.retryInterval);
}
// 超时处理
console.error(`页面加载超时: ${pageName}, 重试次数: ${config.maxRetries}`);
return false;
} catch (error) {
// 任何错误都记录并返回false
console.error(`页面加载出错: ${pageName}, 错误信息: ${error.message}`);
return false;
}
}
/**
* 关闭当前活动的标签页
* @param {string} pageName 页面名称,用于日志显示
* @returns {Promise<boolean>} 是否成功关闭标签页
*/
async closeActiveTab(pageName) {
try {
console.log(`🗑️ 正在关闭页面 "${pageName}" 的tab...`);
// 检查是否存在活动的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 hasActiveTab = await activeTab.count() > 0;
const hasCloseButton = await closeButton.count() > 0;
if (hasActiveTab && hasCloseButton) {
// 确保关闭按钮可见并点击
await closeButton.waitFor({state: 'visible', timeout: 5000});
await closeButton.click();
// 等待关闭动画完成
await this.page.waitForTimeout(500);
return true;
} else {
console.log(`⚠️ [${pageName}] 没有找到可关闭的tab继续执行...`);
return false;
}
} catch (error) {
console.error(`关闭标签页时出错 [${pageName}]:`, error.message);
return false;
}
}
/**
* 获取三级菜单列表
* @param {Object} parentMenu 父级菜单对象
* @returns {Promise<Array>} 三级菜单项数组
* @private
*/
async getThirdLevelMenus(parentMenu) {
try {
// 检查三级菜单是否存在
const elements = await this.page.locator('.el-popper.is-light.el-popover .menuTitle.canClick').all();
if (elements.length === 0) {
return [];
}
// 收集所有三级菜单项
const thirdMenus = [];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const text = await element.textContent();
thirdMenus.push({
index: i,
text: text.trim(),
element: element,
hasThirdMenu: false, // 三级菜单项不会有下一级
uniqueId: `menu_${parentMenu.uniqueId}_${i}_${text.trim().replace(/\s+/g, '_')}`,
path: `${parentMenu.text} > ${text.trim()}`
});
}
return thirdMenus;
} catch (error) {
console.error(`获取三级菜单失败: ${error.message}`);
return [];
}
}
/**
* 处理三级菜单
* @param {Object} menu 菜单对象
*/
async handleThreeLevelMenu(menu) {
console.log(`处理三级菜单: ${menu.text}`);
// 1. 点击展开三级菜单
await menu.element.click();
await this.page.waitForTimeout(500);
// 2. 获取三级菜单列表
const thirdMenus = await this.getThirdLevelMenus(menu);
if (thirdMenus.length === 0) {
console.log(`未找到三级菜单项`);
return;
}
// 3. 处理每个三级菜单项
for (let i = 0; i < thirdMenus.length; i++) {
const thirdMenu = thirdMenus[i];
console.log(`处理第 ${i + 1}/${thirdMenus.length} 个三级菜单项: ${thirdMenu.text}`);
await this.handleMenuClick(thirdMenu, menu);
// 如果还有下一个菜单项,重新展开三级菜单
if (i < thirdMenus.length - 1) {
await menu.element.click();
await this.page.waitForTimeout(500);
}
}
}
/**
* 处理菜单点击和页面加载
* @param {Object} menuInfo 菜单信息对象
* @param {Object} parentMenu 父级菜单(可选)
*/
async handleMenuClick(menuInfo, parentMenu = null) {
const menuPath = parentMenu ? `${parentMenu.text} > ${menuInfo.text}` : menuInfo.text;
console.log(`点击菜单: ${menuPath}`);
// 1. 点击菜单并等待页面加载
await menuInfo.element.click();
const loadResult = await this.waitForPageLoadWithRetry(menuInfo);
if (!loadResult) {
console.warn(`页面加载失败: ${menuPath}`);
return false;
}
// 2. 处理页面中的标签页
await this.handleAllTabs(menuInfo);
// 3. 关闭当前标签页
await this.closeActiveTab(menuPath);
return true;
}
/**
* 处理单个TAB页
* @param {Object} tabInfo TAB页信息对象包含text、isActive和element属性
* @param {Object} parentMenu 父级菜单对象
* @returns {Promise<boolean>} 处理是否成功
* @private
*/
async handleSingleTab(tabInfo, parentMenu) {
try {
const menuPath = parentMenu.path || parentMenu.text;
console.log(`🔹 处理TAB页: ${menuPath} > ${tabInfo.text}`);
// 直接使用传入的element点击
await tabInfo.element.click();
return !await this.waitForPageLoadWithRetry(parentMenu, tabInfo.text);
} catch (error) {
console.error(`处理TAB页失败 [${parentMenu.text} > ${tabInfo.text}]:`, error.message);
return false;
}
}
/**
* 处理所有TAB页
* @param {Object} menu 菜单对象
* @returns {Promise<boolean>} 处理是否成功
* @private
*/
async handleAllTabs(menu) {
try {
// 等待TAB容器加载
await this.page.waitForTimeout(1000);
// 使用更精确的选择器获取工作区的TAB页
const tabs = await this.page.locator('.workSpaceBaseTab .el-tabs__item').all();
if (tabs.length === 0) {
console.log(`📝 ${menu.text} 没有TAB页`);
return true;
}
console.log(`📑 ${menu.text} 找到 ${tabs.length} 个TAB页`);
// 获取所有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) {
// 跳过当前激活的TAB页因为它已经是默认加载的
if (!tabInfo.isActive) {
await this.handleSingleTab(tabInfo, menu);
} else {
console.log(`⏭️ 跳过当前激活的TAB页: ${menu.text} > ${tabInfo.text}`);
}
}
return true;
} catch (error) {
console.error(`处理TAB页失败 [${menu.text}]:`, error.message);
return false;
}
}
}
module.exports = LongiMainPage;