playwright/tests/pages/LongiMainPage.js
2025-03-06 10:14:41 +08:00

626 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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() {
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',
// 加载状态
loadingMask: '.el-loading-mask',
errorBox: '.el-message-box__message',
errorMessage: '.el-message--error',
// 临时元素
temporaryElements: '.el-loading-mask, .el-message, .el-message-box'
};
}
/**
* 初始化配置
* @private
*/
initializeConfig() {
// 调用父类的配置初始化
super.initializeConfig();
// 添加或覆盖特定于 LongiMainPage 的配置
Object.assign(this.config, {
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 = 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;
}
/**
* 获取菜单项的路径信息
* @param {Object} menuItem 菜单项元素
* @returns {Promise<string>} 菜单路径
*/
async getMenuPath(menuItem) {
// 尝试获取父级菜单的文本
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();
return parentText ? `${parentText} > ${itemText}` : itemText;
}
/**
* 处理所有菜单点击
* @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 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, menuPath)) {
return;
}
if (!await this.waitForPageLoadWithRetry(menuInfo)) {
console.warn(`页面加载失败: ${menuPath}`);
return;
}
await this.handleAllTabs(menuInfo);
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页信息对象
* @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<Object>} tabs TAB页元素数组
* @returns {Promise<Array>} 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 菜单对象
* @param {string} subMenuText 子菜单文本(可选)
* @returns {Promise<boolean>} 页面是否加载成功
*/
async waitForPageLoadWithRetry(menu, subMenuText = '') {
const pageName = this.getMenuPath(menu, {text: subMenuText});
console.log(`等待页面 ${pageName} 数据加载...`);
let retryCount = 0;
const {maxRetries, retryInterval, stabilityDelay} = this.config.pageLoad;
try {
while (retryCount < maxRetries) {
// 检查错误状态
const hasError = await this.checkPageError(pageName);
if (hasError) return false;
// 检查加载状态
const isLoading = await this.elementExists(this.selectors.loadingMask);
if (!isLoading) {
await this.wait(stabilityDelay);
console.log(`✅ 页面 ${pageName} 加载完成`);
return true;
}
retryCount++;
await this.wait(retryInterval);
}
console.error(`页面加载超时: ${pageName}, 重试次数: ${maxRetries}`);
return false;
} catch (error) {
console.error(`页面加载出错: ${pageName}, 错误信息: ${error.message}`);
return false;
}
}
/**
* 检查页面是否有错误
* @param {string} pageName 页面名称
* @returns {Promise<boolean>} 是否有错误
* @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 页面名称,用于日志显示
*/
async closeActiveTab(pageName) {
try {
console.log(`🗑️ 正在关闭页面 "${pageName}" 的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, `${pageName}的关闭按钮`);
await this.wait(500);
} else {
console.log(`⚠️ [${pageName}] 没有找到可关闭的tab继续执行...`);
}
} catch (error) {
console.error(`关闭标签页时出错 [${pageName}]:`, 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;