diff --git a/tests/pages/LongiMainPage.js b/tests/pages/LongiMainPage.js index 8bac651..5df927f 100644 --- a/tests/pages/LongiMainPage.js +++ b/tests/pages/LongiMainPage.js @@ -11,23 +11,89 @@ class LongiMainPage extends BasePage { */ constructor(page) { super(page); + this.initializeSelectors(); + this.initializeConfig(); + } - // 页面元素选择器 + /** + * 初始化选择器 + * @private + */ + initializeSelectors() { 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' // 子菜单指示器 + // 侧边导航菜单 + 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', + // 加载状态 + loadingMask: '.el-loading-mask', + errorBox: '.el-message-box__message', + errorMessage: '.el-message--error', + // 临时元素 + temporaryElements: '.el-loading-mask, .el-message, .el-message-box' }; + } - // 设置超时时间 - this.timeout = 10000; + /** + * 初始化配置 + * @private + */ + initializeConfig() { + this.config = { + timeout: 10000, + pageLoad: { + maxRetries: 30, + retryInterval: 500, + stabilityDelay: 1000 + }, + menuTimeout: parseInt(process.env.MENU_TIME_OUT || '30000', 10) + }; + } + + /** + * 等待指定时间 + * @param {number} ms 等待时间(毫秒) + * @private + */ + async wait(ms) { + await this.page.waitForTimeout(ms); + } + + /** + * 检查元素是否存在 + * @param {string} selector 元素选择器 + * @returns {Promise} 是否存在 + * @private + */ + async elementExists(selector) { + const count = await this.page.locator(selector).count(); + return count > 0; + } + + /** + * 获取元素文本 + * @param {Object} element Playwright元素对象 + * @returns {Promise} 元素文本 + * @private + */ + async getElementText(element) { + try { + const text = await element.textContent(); + return text.trim(); + } catch (error) { + console.error('获取元素文本失败:', error.message); + return ''; + } } /** @@ -51,27 +117,16 @@ class LongiMainPage extends BasePage { /** * 点击展开菜单 - * @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); + 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('已点击展开菜单'); } /** @@ -334,33 +389,6 @@ class LongiMainPage extends BasePage { 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); - } - } - /** * 获取三级菜单列表 * @param {Object} parentMenu 父级菜单对象 @@ -432,25 +460,33 @@ class LongiMainPage extends BasePage { * @param {Object} parentMenu 父级菜单(可选) */ async handleMenuClick(menuInfo, parentMenu = null) { - const menuPath = parentMenu ? `${parentMenu.text} > ${menuInfo.text}` : menuInfo.text; + const menuPath = this.getMenuPath(menuInfo, parentMenu); console.log(`点击菜单: ${menuPath}`); - // 1. 点击菜单并等待页面加载 - await menuInfo.element.click(); - const isLoaded = await this.waitForPageLoadWithRetry(menuInfo); + if (!await this.safeClick(menuInfo.element, menuPath)) { + return; + } - if (!isLoaded) { + if (!await this.waitForPageLoadWithRetry(menuInfo)) { console.warn(`页面加载失败: ${menuPath}`); return; } - // 2. 处理页面中的标签页 await this.handleAllTabs(menuInfo); - - // 3. 关闭当前标签页 await this.closeActiveTab(menuPath); } + /** + * 获取菜单路径 + * @param {Object} menuInfo 菜单信息对象 + * @param {Object} parentMenu 父级菜单(可选) + * @returns {string} 菜单路径 + * @private + */ + getMenuPath(menuInfo, parentMenu = null) { + return parentMenu ? `${parentMenu.text} > ${menuInfo.text}` : menuInfo.text; + } + /** * 处理单个TAB页 * @param {Object} tabInfo TAB页信息对象 @@ -474,28 +510,17 @@ class LongiMainPage extends BasePage { */ async handleAllTabs(menu) { try { - // 等待TAB容器加载 - await this.page.waitForTimeout(1000); + await this.wait(1000); - // 使用更精确的选择器获取工作区的TAB页 - const tabs = await this.page.locator('.workSpaceBaseTab .el-tabs__item').all(); + 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); - // 获取所有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) { if (!tabInfo.isActive) { await this.handleSingleTab(tabInfo, menu); @@ -508,6 +533,22 @@ class LongiMainPage extends BasePage { } } + /** + * 获取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 + })) + ); + } + /** * 等待页面加载完成,带重试机制 * @param {Object} menu 菜单对象 @@ -515,61 +556,59 @@ class LongiMainPage extends BasePage { * @returns {Promise} 页面是否加载成功 */ async waitForPageLoadWithRetry(menu, subMenuText = '') { - const pageName = subMenuText ? `${menu.text} > ${subMenuText}` : menu.text; + const pageName = this.getMenuPath(menu, { text: subMenuText }); console.log(`等待页面 ${pageName} 数据加载...`); - const config = { - maxRetries: 30, - retryInterval: 500, - stabilityDelay: 1000 - }; - let retryCount = 0; + const { maxRetries, retryInterval, stabilityDelay } = this.config.pageLoad; try { - while (retryCount < config.maxRetries) { - // 检查页面状态 - const selectors = { - loadingMask: '.el-loading-mask', - errorBox: '.el-message-box__message', - errorMessage: '.el-message--error' - }; - + while (retryCount < maxRetries) { // 检查错误状态 - 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 hasError = await this.checkPageError(pageName); + if (hasError) return false; // 检查加载状态 - const loadingMasks = await this.page.locator(selectors.loadingMask).all(); - if (loadingMasks.length === 0) { - // 加载完成,等待页面稳定 - await this.page.waitForTimeout(config.stabilityDelay); + const isLoading = await this.elementExists(this.selectors.loadingMask); + if (!isLoading) { + await this.wait(stabilityDelay); console.log(`✅ 页面 ${pageName} 加载完成`); return true; } - // 继续等待 retryCount++; - await this.page.waitForTimeout(config.retryInterval); + await this.wait(retryInterval); } - // 超时处理 - console.error(`页面加载超时: ${pageName}, 重试次数: ${config.maxRetries}`); + console.error(`页面加载超时: ${pageName}, 重试次数: ${maxRetries}`); return false; - } catch (error) { - // 任何错误都记录并返回false console.error(`页面加载出错: ${pageName}, 错误信息: ${error.message}`); return false; } } + /** + * 检查页面是否有错误 + * @param {string} pageName 页面名称 + * @returns {Promise} 是否有错误 + * @private + */ + async checkPageError(pageName) { + const errorSelectors = [this.selectors.errorBox, this.selectors.errorMessage]; + + for (const selector of errorSelectors) { + const elements = await this.page.locator(selector).all(); + if (elements.length > 0) { + const errorText = await this.getElementText(elements[0]); + console.error(`页面加载出现错误: ${pageName}, 错误信息: ${errorText}`); + return true; + } + } + + return false; + } + /** * 关闭当前活动的标签页 * @param {string} pageName 页面名称,用于日志显示 @@ -578,16 +617,13 @@ class LongiMainPage extends BasePage { try { console.log(`🗑️ 正在关闭页面 "${pageName}" 的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 activeTab = this.page.locator(this.selectors.activeTab); + const closeButton = activeTab.locator(this.selectors.closeButton); - const hasActiveTab = await activeTab.count() > 0; - const hasCloseButton = await closeButton.count() > 0; - - if (hasActiveTab && hasCloseButton) { + if (await this.canCloseTab(activeTab, closeButton)) { await closeButton.waitFor({state: 'visible', timeout: 5000}); - await closeButton.click(); - await this.page.waitForTimeout(500); + await this.safeClick(closeButton, `${pageName}的关闭按钮`); + await this.wait(500); } else { console.log(`⚠️ [${pageName}] 没有找到可关闭的tab,继续执行...`); } @@ -595,6 +631,37 @@ class LongiMainPage extends BasePage { console.error(`关闭标签页时出错 [${pageName}]:`, 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; \ No newline at end of file diff --git a/tests/utils/BasePage.js b/tests/utils/BasePage.js index 18e46fa..4949e9c 100644 --- a/tests/utils/BasePage.js +++ b/tests/utils/BasePage.js @@ -28,12 +28,28 @@ class BasePage { * 等待元素可见 * @param {string} selector 元素选择器 * @param {Object} options 选项 - * @returns {Promise} 元素定位器 + * @param {boolean} [options.returnBoolean=false] 是否返回布尔值而不是元素 + * @param {boolean} [options.firstOnly=false] 是否只获取第一个元素 + * @returns {Promise} 元素定位器或是否可见 */ async waitForElement(selector, options = {}) { - const element = this.page.locator(selector); - await element.waitFor({state: 'visible', timeout: options.timeout || this.timeout}); - return element; + try { + const element = options.firstOnly ? + this.page.locator(selector).first() : + this.page.locator(selector); + + await element.waitFor({ + state: 'visible', + timeout: options.timeout || this.timeout + }); + + return options.returnBoolean ? true : element; + } catch (error) { + if (options.returnBoolean) { + return false; + } + throw error; + } } /** @@ -145,6 +161,22 @@ class BasePage { const element = await this.waitForElement(selector); return element.getAttribute(attributeName); } + + /** + * 安全点击元素 + * @param {Object} element Playwright元素对象 + * @param {string} description 元素描述,用于日志 + * @returns {Promise} 是否点击成功 + */ + async safeClick(element, description) { + try { + await element.click(); + return true; + } catch (error) { + console.error(`点击${description}失败:`, error.message); + return false; + } + } } module.exports = BasePage; \ No newline at end of file