/** * 龙蛟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.MENU_DATA_FILE_PATH} 成功加载了 ${menuItems.length} 个菜单项`); //这里因为是序列化回来的,所以没有对应每个menu的element元素,需要补充回来。 console.log('开始恢复菜单项的element元素...'); // 等待菜单加载完成 await this.page.waitForSelector(this.selectors.sideNav, { timeout: parseInt(process.env.MENU_TIME_OUT || '30000', 10) }); // 获取当前页面上的所有菜单元素 const currentMenuElements = await this.page.locator(this.selectors.menuItems).all(); console.log(`当前页面上找到 ${currentMenuElements.length} 个菜单元素`); // 为每个菜单项恢复element元素 for (const menuItem of menuItems) { 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元素`); } } // 检查是否所有菜单项都恢复了element元素 const missingElementCount = menuItems.filter(item => !item.element).length; if (missingElementCount > 0) { console.warn(`⚠️ 有 ${missingElementCount} 个菜单项未能恢复element元素`); // 如果大部分菜单项都没有恢复element元素,可能需要重新查找 if (missingElementCount > menuItems.length / 2) { console.log('由于大部分菜单项未能恢复element元素,将重新查找所有菜单项'); await this.expandSideMenu(); return await this.findAndSaveMenuItems(); } } 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 ''; } } /** * 通过索引查找菜单项 * @param {number} index 菜单项索引 * @param {Array} menuItems 菜单项数组,如果未提供则会调用findMenuItems获取 * @returns {Promise} 找到的菜单项或null */ async findMenuItemByIndex(index, menuItems = null) { try { // 如果未提供菜单项数组,则获取 if (!menuItems) { menuItems = await this.findMenuItems(); } // 查找指定索引的菜单项 const menuItem = menuItems.find(item => item.index === index); if (menuItem) { console.log(`通过索引 ${index} 找到菜单项: "${menuItem.text}"`); return menuItem; } else { console.log(`未找到索引为 ${index} 的菜单项`); return null; } } catch (error) { console.error(`通过索引查找菜单项时出错: ${error}`); return null; } } /** * 通过文本和可选的索引查找菜单项 * 当有多个同名菜单项时,可以通过指定occurrence来选择第几个匹配项 * @param {string} text 菜单项文本 * @param {number} occurrence 第几个匹配项,从0开始计数,默认为0(第一个) * @param {Array} menuItems 菜单项数组,如果未提供则会调用findMenuItems获取 * @returns {Promise} 找到的菜单项或null */ async findMenuItemByText(text, occurrence = 0, menuItems = null) { try { // 如果未提供菜单项数组,则获取 if (!menuItems) { menuItems = await this.findMenuItems(); } // 查找所有匹配文本的菜单项 const matchingItems = menuItems.filter(item => item.text === text); if (matchingItems.length > 0) { if (occurrence < matchingItems.length) { const menuItem = matchingItems[occurrence]; console.log(`通过文本 "${text}" 找到第 ${occurrence + 1} 个菜单项,索引为 ${menuItem.index}`); return menuItem; } else { console.log(`未找到第 ${occurrence + 1} 个文本为 "${text}" 的菜单项,只有 ${matchingItems.length} 个匹配项`); return null; } } else { console.log(`未找到文本为 "${text}" 的菜单项`); return null; } } catch (error) { console.error(`通过文本查找菜单项时出错: ${error}`); return null; } } /** * 通过唯一ID查找菜单项 * @param {string} uniqueId 菜单项的唯一ID * @param {Array} menuItems 菜单项数组,如果未提供则会调用findMenuItems获取 * @returns {Promise} 找到的菜单项或null */ async findMenuItemByUniqueId(uniqueId, menuItems = null) { try { // 如果未提供菜单项数组,则获取 if (!menuItems) { menuItems = await this.findMenuItems(); } // 查找指定唯一ID的菜单项 const menuItem = menuItems.find(item => item.uniqueId === uniqueId); if (menuItem) { console.log(`通过唯一ID ${uniqueId} 找到菜单项: "${menuItem.text}"`); return menuItem; } else { console.log(`未找到唯一ID为 ${uniqueId} 的菜单项`); return null; } } catch (error) { console.error(`通过唯一ID查找菜单项时出错: ${error}`); return null; } } /** * 检查是否存在三级菜单并进行相应处理 * @param {Object} menuItem 菜单项对象,包含索引、文本和元素 * @param {number} timeout 超时时间(毫秒),默认30秒 * @returns {Promise<{hasThirdMenu: boolean, thirdMenuItems: Array}>} 是否有三级菜单及三级菜单项数组 */ async checkForThirdLevelMenu(menuItem, timeout = 30000) { try { console.log(`点击菜单项 "${menuItem.text}" 检查三级菜单...`); // 点击菜单项,触发三级菜单弹出 await menuItem.element.click(); // 等待弹出动画完成,增加等待时间 await this.page.waitForTimeout(2000); // 检查三级菜单 const thirdMenuSelector = this.selectors.thirdLevelMenu; // 尝试等待三级菜单出现,但不抛出错误 const hasThirdMenu = await this.page.locator(thirdMenuSelector).count().then(count => count > 0).catch(() => false); // 如果没有立即找到,再等待一段时间再次检查 let thirdMenuCount = 0; if (hasThirdMenu) { thirdMenuCount = await this.page.locator(thirdMenuSelector).count(); } else { // 再等待一段时间,有些菜单可能加载较慢 await this.page.waitForTimeout(1000); thirdMenuCount = await this.page.locator(thirdMenuSelector).count(); } console.log(`菜单项 "${menuItem.text}" ${thirdMenuCount > 0 ? '有' : '没有'}三级菜单,找到 ${thirdMenuCount} 个三级菜单项`); // 如果没有三级菜单,直接返回 if (thirdMenuCount === 0) { return {hasThirdMenu: false, thirdMenuItems: []}; } // 收集三级菜单项 const thirdMenuItems = []; for (let i = 0; i < thirdMenuCount; i++) { const item = this.page.locator(thirdMenuSelector).nth(i); const text = await item.textContent(); thirdMenuItems.push({ index: i, text: text.trim(), element: item }); } // 输出三级菜单项文本 const menuTexts = thirdMenuItems.map(item => item.text).join(', '); console.log(`三级菜单项: ${menuTexts}`); return {hasThirdMenu: true, thirdMenuItems}; } catch (error) { console.error(`检查三级菜单时出错: ${error}`); return {hasThirdMenu: false, thirdMenuItems: []}; } } /** * 处理菜单项,包括检查和处理可能的三级菜单 * @param {Object} menuItem 菜单项对象,包含索引、文本和元素以及hasThirdMenu标志 * @param {Function} processWithThirdMenu 处理有三级菜单情况的回调函数 * @param {Function} processWithoutThirdMenu 处理没有三级菜单情况的回调函数 * @returns {Promise<{hasThirdMenu: boolean, thirdMenuItems: Array}>} 处理结果 */ async processMenuItem(menuItem, processWithThirdMenu = null, processWithoutThirdMenu = null) { try { console.log(`处理菜单项: "${menuItem.text}"`); // 使用菜单项中的hasThirdMenu字段 const hasThirdMenu = menuItem.hasThirdMenu; // 点击菜单项 await menuItem.element.click(); await this.page.waitForTimeout(1000); // 等待可能的动画完成 let thirdMenuItems = []; // 如果有三级菜单,获取三级菜单项 if (hasThirdMenu) { const thirdMenuSelector = this.selectors.thirdLevelMenu; const thirdMenuCount = await this.page.locator(thirdMenuSelector).count(); console.log(`菜单项 "${menuItem.text}" 有三级菜单,找到 ${thirdMenuCount} 个三级菜单项`); // 收集三级菜单项 for (let i = 0; i < thirdMenuCount; i++) { const item = this.page.locator(thirdMenuSelector).nth(i); const text = await item.textContent(); thirdMenuItems.push({ index: i, text: text.trim(), element: item }); } // 输出三级菜单项文本 const menuTexts = thirdMenuItems.map(item => item.text).join(', '); console.log(`三级菜单项: ${menuTexts}`); } else { console.log(`菜单项 "${menuItem.text}" 没有三级菜单`); } // 根据是否有三级菜单调用相应的处理函数 if (hasThirdMenu && processWithThirdMenu) { await processWithThirdMenu(menuItem, thirdMenuItems); } else if (!hasThirdMenu && processWithoutThirdMenu) { await processWithoutThirdMenu(menuItem); } return {hasThirdMenu, thirdMenuItems}; } catch (error) { console.error(`处理菜单项时出错: ${error}`); return {hasThirdMenu: false, thirdMenuItems: []}; } } async handleAllMenus(menuItems) { for (let i = 0; i < menuItems.length; i++) { await this.handleSingleMenu(menuItems[i]); } } async handleSingleMenu(menu) { await this.expandSideMenu(); if (menu.hasThirdMenu) { await this.handleThreeLevelMenu(menu); } else { // 处理二级菜单点击 // await this.handleMenuClick(menu); } } async waitForPageLoadWithRetry(menu, subMenuText = '') { const pageName = subMenuText ? `${menu.text} > ${subMenuText}` : menu.text; const pageStartTime = Date.now(); console.log(`等待页面 ${pageName} 数据加载...`); const config = { maxRetries: 30, retryInterval: 500 }; let retryCount = 0; const check = async () => { try { // 如果超过最大重试次数,记录失败并继续 if (retryCount >= config.maxRetries) { const reason = `页面加载超时 (${config.maxRetries} 次重试)`; this.recordFailedPage(pageName, reason, pageStartTime); return {success: false, error: reason}; } // 检查页面状态 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(); const reason = `页面加载出现错误: ${errorText}`; this.recordFailedPage(pageName, reason, pageStartTime); return {success: false, error: reason}; } } // 检查加载状态 const loadingMasks = await this.page.locator(selectors.loadingMask).all(); if (loadingMasks.length > 0) { retryCount++; await this.page.waitForTimeout(config.retryInterval); return await check(); } // 加载完成,等待页面稳定 await this.page.waitForTimeout(1000); return {success: true, error: null}; } catch (error) { // 如果是页面关闭错误,直接返回 if (error.message.includes('Target page, context or browser has been closed')) { return {success: false, error: '页面已关闭'}; } // 其他错误继续重试 retryCount++; await this.page.waitForTimeout(config.retryInterval); return await check(); } }; return await check(); } // 添加记录失败页面的方法 recordFailedPage(pageName, reason, startTime) { const duration = Date.now() - startTime; console.error(`页面加载失败: ${pageName}, 原因: ${reason}, 耗时: ${duration}ms`); // 这里可以添加将失败信息写入文件的逻辑 } /** * 获取二级菜单元素 * @private */ async getSecondLevelMenu(menu) { const menuElement = await this.page.locator(this.selectors.menuItems).nth(menu.index); await menuElement.click(); await this.page.waitForTimeout(500); // 减少等待时间 return menuElement; } /** * 获取三级菜单项 * @private */ async getThirdLevelMenuItems(menuElement) { const thirdMenuItems = await this.page.locator(this.selectors.thirdLevelMenu).all(); const thirdMenuTexts = await Promise.all( thirdMenuItems.map(item => item.textContent()) ); return { thirdMenuTexts: thirdMenuTexts.map(text => text.trim()), totalItems: thirdMenuItems.length }; } /** * 关闭当前活动的标签页 * @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} menu 菜单对象 */ async handleThreeLevelMenu(menu) { try { console.log(`正在处理 ${menu.text}菜单`) await menu.element.click(); // 等待一个短暂的时间让弹出层出现 await this.page.waitForTimeout(500); // 检查三级菜单是否存在 const hasThirdMenu = await this.page.evaluate(() => { const elements = document.querySelectorAll('.el-popper.is-light.el-popover .menuTitle.canClick'); return elements.length > 0; }); if (!hasThirdMenu) { console.log(`⚠️ ${menu.text} 没有找到三级菜单项`); return; } console.log(`✓ ${menu.text} 找到三级菜单`); } catch (error) { console.error(`处理三级菜单时出错 [${menu.text}]:`, error.message); throw error; } } /** * 通用的菜单点击和页面加载处理方法 * @param {Object} menuInfo 菜单信息对象,包含text属性 * @param {string} parentText 父级菜单文本(可选) * @returns {Promise} 处理是否成功 */ async handleMenuClick(menuInfo, parentText = '') { try { const menuPath = parentText ? `${parentText} > ${menuInfo.text}` : menuInfo.text; console.log(`🔸 点击菜单: ${menuPath}`); // 点击菜单项 if (menuInfo.element) { await menuInfo.element.click(); } else { // 如果没有element属性,尝试通过文本定位 const menuLocator = this.page.locator(`text=${menuInfo.text}`).first(); await menuLocator.click(); } // 等待页面加载 const loadResult = await this.waitForPageLoadWithRetry({text: menuPath}); if (!loadResult.success) { console.warn(loadResult.error); return false; } // 关闭标签页 await this.closeActiveTab(menuPath); return true; } catch (error) { console.error(`处理菜单点击失败 [${menuInfo.text}]:`, error.message); return false; } } } module.exports = LongiMainPage;