playwright/tests/pages/LongiMainPage.js
2025-03-05 16:21:04 +08:00

776 lines
30 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.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<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);
} 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<boolean>} 是否成功关闭标签页
*/
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<boolean>} 处理是否成功
*/
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;