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, { menuTimeout: parseInt(process.env.MENU_TIME_OUT || '30000', 10) }); } /** * 检查菜单是否已展开 * @returns {Promise} 菜单是否已展开 */ 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} 菜单项数组 */ 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 */ async 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} 返回更新后的菜单项 * @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} 三级菜单项数组 * @private */ async getThirdLevelMenus(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.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 = this.getMenuPath(menuInfo, parentMenu); console.log(`点击菜单: ${menuPath}`); if (!await this.safeClick(menuInfo.element)) { return; } if (!await this.waitForPageLoadWithRetry(menuInfo)) { 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; console.log(`🔹 处理TAB页: ${menuPath} > ${tabInfo.text}`); await tabInfo.element.click(); await this.waitForPageLoadWithRetry(parentMenu, tabInfo.text); } catch (error) { console.error(`处理TAB页失败 [${parentMenu.text} > ${tabInfo.text}]:`, error.message); } } /** * 处理所有TAB页 * @param {Object} menu 菜单对象 */ async handleAllTabs(menu) { try { await this.wait(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.getTabInfos(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} tabs TAB页元素数组 * @returns {Promise} 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 })) ); } /** * 关闭当前活动的标签页 */ 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.safeClick(closeButton); await this.wait(500); } else { console.log(`⚠️ [${parentMenu.text}] 没有找到可关闭的tab,继续执行...`); } } catch (error) { console.error(`关闭标签页时出错 [${parentMenu.text}]:`, error.message); } } /** * 检查是否可以关闭标签页 * @param {Object} activeTab 活动标签页元素 * @param {Object} closeButton 关闭按钮元素 * @returns {Promise} 是否可以关闭 * @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;