/** * 龙蛟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} 菜单是否已展开 */ 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} */ 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} 是否执行了展开操作 */ 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} 菜单项数组 */ 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} 菜单路径 */ 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} 返回更新后的菜单项 * @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} * @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} 页面是否加载成功 */ 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} 是否成功关闭标签页 */ 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} 三级菜单项数组 * @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} 处理是否成功 * @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} 处理是否成功 * @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;