542 lines
18 KiB
JavaScript
542 lines
18 KiB
JavaScript
const BasePage = require('./BasePage');
|
||
const FileUtils = require('../utils/FileUtils');
|
||
|
||
class LongiMainPage extends BasePage {
|
||
/**
|
||
* 创建主页面对象
|
||
* @param {import('@playwright/test').Page} page Playwright页面对象
|
||
*/
|
||
constructor(page) {
|
||
super(page);
|
||
this.initializeSelectors();
|
||
this.initializeConfig();
|
||
}
|
||
|
||
/**
|
||
* 初始化选择器
|
||
* @private
|
||
*/
|
||
initializeSelectors() {
|
||
// 调用父类的选择器初始化
|
||
super.initializeSelectors();
|
||
|
||
// 添加或覆盖特定于 LongiMainPage 的选择器
|
||
Object.assign(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',
|
||
// Tab相关
|
||
tabContainer: '.workSpaceBaseTab .el-tabs__item',
|
||
activeTab: '.vab-tabs .el-tabs--card .el-tabs__item.is-active',
|
||
closeButton: '.el-icon.is-icon-close'
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 初始化配置
|
||
* @private
|
||
*/
|
||
initializeConfig() {
|
||
// 调用父类的配置初始化
|
||
super.initializeConfig();
|
||
|
||
// 添加或覆盖特定于 LongiMainPage 的配置
|
||
Object.assign(this.config, {
|
||
timeout: {
|
||
menuTimeout: parseInt(process.env.MENU_TIME_OUT || '30000', 10)
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 检查菜单是否已展开
|
||
* @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';
|
||
}
|
||
|
||
/**
|
||
* 点击展开菜单
|
||
*/
|
||
async clickExpandMenu() {
|
||
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('已点击展开菜单');
|
||
}
|
||
|
||
/**
|
||
* 检查并展开侧边菜单
|
||
*/
|
||
async expandSideMenu() {
|
||
// 检查菜单是否已展开
|
||
const isExpanded = await this.isMenuExpanded();
|
||
|
||
if (!isExpanded) {
|
||
console.log('菜单未展开,点击展开');
|
||
await this.clickExpandMenu();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查菜单数据文件是否存在并加载数据
|
||
*/
|
||
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() {
|
||
// 查找菜单项
|
||
const menuItems = await this.findMenuItems();
|
||
|
||
// 如果没有找到菜单项,则返回空数组
|
||
if (!menuItems || menuItems.length === 0) {
|
||
console.warn('未找到任何菜单项,无法保存到文件');
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
// 过滤掉不能序列化的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 menuItems; // 即使保存失败也返回找到的菜单项
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查找所有菜单项
|
||
* @returns {Promise<Array>} 菜单项数组
|
||
*/
|
||
async findMenuItems() {
|
||
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 = this.getMenuPath(item);
|
||
|
||
menuItems.push({
|
||
index: i,
|
||
text: text.trim(),
|
||
element: item,
|
||
hasThirdMenu: hasThirdMenu,
|
||
uniqueId: uniqueId,
|
||
path: menuPath
|
||
});
|
||
}
|
||
}
|
||
|
||
console.log(`🔍 找到 ${menuItems.length} 个可测试的菜单项`);
|
||
return menuItems;
|
||
}
|
||
|
||
/**
|
||
* 获取菜单路径
|
||
* @param {Object} menuInfo 菜单信息对象
|
||
* @param {Object} parentMenu 父级菜单(可选)
|
||
* @returns {string} 菜单路径
|
||
* @private
|
||
*/
|
||
getMenuPath(menuInfo, parentMenu = null) {
|
||
return parentMenu ? `${parentMenu.text} > ${menuInfo.text}` : menuInfo.text;
|
||
}
|
||
|
||
/**
|
||
* 处理所有菜单点击
|
||
* @param {Array} menuItems 菜单项数组
|
||
*/
|
||
async handleAllMenuClicks(menuItems) {
|
||
try {
|
||
for (let i = 0; i < menuItems.length; i++) {
|
||
await this.handleSingleMenuClick(menuItems[i]);
|
||
}
|
||
} catch (error) {
|
||
console.error('处理菜单点击时出错:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理单个菜单点击
|
||
* @param {Object} menu 菜单对象
|
||
*/
|
||
async handleSingleMenuClick(menu) {
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* 恢复菜单项的element元素
|
||
* @param {Object} menuItem 需要恢复element的菜单项
|
||
* @returns {Promise<Object>} 返回更新后的菜单项
|
||
* @private
|
||
*/
|
||
async restoreMenuElement(menuItem) {
|
||
// 获取当前页面上的所有菜单元素
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 获取三级菜单列表
|
||
* @param {Object} parentMenu 父级菜单对象
|
||
* @returns {Promise<Array>} 三级菜单项数组
|
||
* @private
|
||
*/
|
||
async findThirdLevelMenus(parentMenu) {
|
||
// 检查三级菜单是否存在
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 处理三级菜单
|
||
* @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.findThirdLevelMenus(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 = await this.getMenuPath(menuInfo, parentMenu);
|
||
console.log(`点击菜单: ${menuPath}`);
|
||
|
||
if (!await this.clickByElement(menuInfo.element)) {
|
||
return;
|
||
}
|
||
|
||
if (!await this.waitForIBPPageLoadWithRetry(menuPath)) {
|
||
console.warn(`页面加载失败: ${menuPath}`);
|
||
return;
|
||
}
|
||
|
||
await this.handleAllTabs(menuInfo);
|
||
await this.closeActiveTab(menuInfo);
|
||
}
|
||
|
||
/**
|
||
* 处理单个TAB页
|
||
* @param {Object} tabInfo TAB页信息对象
|
||
* @param {Object} parentMenu 父级菜单对象
|
||
*/
|
||
async handleSingleTab(tabInfo, parentMenu) {
|
||
try {
|
||
const menuPath = parentMenu.path || parentMenu.text;
|
||
const tabPath = `${menuPath} > ${tabInfo.text}`;
|
||
console.log(`🔹 处理TAB页: ${tabPath}`);
|
||
|
||
await tabInfo.element.click();
|
||
await this.waitForIBPPageLoadWithRetry(tabPath);
|
||
} catch (error) {
|
||
console.error(`处理TAB页失败 [${parentMenu.text} > ${tabInfo.text}]:`, error.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理所有TAB页
|
||
* @param {Object} menu 菜单对象
|
||
*/
|
||
async handleAllTabs(menu) {
|
||
try {
|
||
await this.waitForTimeout(1000);
|
||
|
||
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.findTabInfos(tabs);
|
||
|
||
for (const tabInfo of tabInfos) {
|
||
if (!tabInfo.isActive) {
|
||
await this.handleSingleTab(tabInfo, menu);
|
||
} else {
|
||
console.log(`⏭️ 跳过当前激活的TAB页: ${menu.text} > ${tabInfo.text}`);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(`处理TAB页失败 [${menu.text}]:`, error.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取TAB页信息
|
||
* @param {Array<Object>} tabs TAB页元素数组
|
||
* @returns {Promise<Array>} TAB页信息数组
|
||
* @private
|
||
*/
|
||
async findTabInfos(tabs) {
|
||
return Promise.all(
|
||
tabs.map(async element => ({
|
||
text: await this.getTextByElement(element),
|
||
isActive: await element.evaluate(el => el.classList.contains('is-active')),
|
||
element: element
|
||
}))
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 关闭当前活动的标签页
|
||
*/
|
||
async closeActiveTab(parentMenu) {
|
||
try {
|
||
console.log(`🗑️ 正在关闭页面 "${parentMenu.text}" 的tab...`);
|
||
|
||
const activeTab = this.page.locator(this.selectors.activeTab);
|
||
const closeButton = activeTab.locator(this.selectors.closeButton);
|
||
|
||
if (await this.canCloseTab(activeTab, closeButton)) {
|
||
await closeButton.waitFor({state: 'visible', timeout: 5000});
|
||
await this.clickByElement(closeButton);
|
||
await this.waitForTimeout(500);
|
||
} else {
|
||
console.log(`⚠️ [${parentMenu.text}] 没有找到可关闭的tab,继续执行...`);
|
||
}
|
||
} catch (error) {
|
||
console.error(`关闭标签页时出错 [${parentMenu.text}]:`, 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; |