playwright/tests/pages/LongiMainPage.js
2025-03-04 17:13:49 +08:00

625 lines
24 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.

/**
* 龙蛟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<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';
}
/**
* 点击展开菜单
* @returns {Promise<void>}
*/
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<boolean>} 是否执行了展开操作
*/
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<Array>} 菜单项数组
*/
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<string>} 菜单路径
*/
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<Object|null>} 找到的菜单项或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<Object|null>} 找到的菜单项或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<Object|null>} 找到的菜单项或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);
}
}
/**
* 等待页面加载完成,包括检查加载遮罩和错误提示
* @param {Object} menu 菜单对象包含text等信息
* @param {string} [subMenuText] 子菜单文本,可选
* @returns {Promise<{success: boolean, error: string|null}>}
*/
async waitForPageLoadWithRetry(menu, subMenuText = '') {
const pageName = subMenuText ? `${menu.text} > ${subMenuText}` : menu.text;
console.log(`等待页面 ${pageName} 数据加载...`);
let retryCount = 0;
const maxRetries = 30;
const retryInterval = 500;
while (retryCount < maxRetries) {
try {
// 检查是否存在加载遮罩
const hasLoadingMask = await this.page.locator('.el-loading-mask').count() > 0;
// 检查是否存在错误提示
const hasErrorBox = await this.page.locator('.el-message-box__message').count() > 0;
const hasErrorMessage = await this.page.locator('.el-message--error').count() > 0;
// 如果存在错误提示,立即返回错误状态
if (hasErrorBox || hasErrorMessage) {
console.log('页面加载出现错误');
let errorMessage = '';
if (hasErrorBox) {
errorMessage = await this.page.locator('.el-message-box__message').textContent();
} else if (hasErrorMessage) {
errorMessage = await this.page.locator('.el-message--error').textContent();
}
return {
success: false,
error: `页面 ${pageName} 加载出现错误: ${errorMessage}`
};
}
// 如果还有加载遮罩,继续等待
if (hasLoadingMask) {
retryCount++;
console.log(`等待加载中... (${retryCount}/${maxRetries})`);
await this.page.waitForTimeout(retryInterval);
continue;
}
// 如果没有加载遮罩也没有错误,说明加载完成
console.log('页面数据加载完成');
await this.page.waitForTimeout(1000); // 额外等待一秒确保页面稳定
return {
success: true,
error: null
};
} catch (error) {
console.error(`检查页面加载状态时出错: ${error}`);
retryCount++;
await this.page.waitForTimeout(retryInterval);
}
}
// 超过最大重试次数
console.log('页面加载超时,继续执行...');
return {
success: false,
error: `页面 ${pageName} 加载超时 (${maxRetries} 次重试)`
};
}
async handleThreeLevelMenu(menu) {
try {
// 使用下标定位菜单元素
const menuElement = await this.page.locator(this.selectors.menuItems).nth(menu.index);
// 首次点击菜单项显示三级菜单
await menuElement.click();
await this.page.waitForTimeout(1000);
// 获取所有三级菜单项的文本和数量信息
const thirdMenuItems = await this.page.locator(this.selectors.thirdLevelMenu).all();
const totalItems = thirdMenuItems.length;
console.log(`${menu.text} 包含 ${totalItems} 个三级菜单`);
// 存储所有三级菜单的文本,因为后续重新获取元素时会需要
const thirdMenuTexts = [];
for (const item of thirdMenuItems) {
thirdMenuTexts.push(await item.textContent());
}
// 处理每个三级菜单项
for (let i = 0; i < totalItems; i++) {
const progress = (((i + 1) / totalItems) * 100).toFixed(1);
const currentMenuText = thirdMenuTexts[i];
console.log(`\n🔸 处理三级菜单 [${i + 1}/${totalItems}] (${progress}%): ${menu.text} > ${currentMenuText}`);
// 只有在处理第二个及以后的菜单项时,才需要重新点击二级菜单
if (i > 0) {
await menuElement.click();
await this.page.waitForTimeout(1000);
}
// 重新获取当前要点击的三级菜单项
const currentThirdMenuItem = await this.page.locator(this.selectors.thirdLevelMenu)
.filter({ hasText: currentMenuText })
.first();
// 点击当前三级菜单项
await currentThirdMenuItem.click();
// 使用新的等待页面加载方法传入menu对象和当前三级菜单文本
const loadResult = await this.waitForPageLoadWithRetry(menu, currentMenuText);
if (!loadResult.success) {
console.warn(loadResult.error);
}
// 等待一段时间后继续处理下一个三级菜单
await this.page.waitForTimeout(1000);
}
console.log(`✅ 完成菜单 "${menu.text}" 的所有三级菜单处理 (100%)`);
} catch (error) {
console.error(`处理三级菜单时出错: ${error}`);
}
}
}
module.exports = LongiMainPage;