playwright/tests/pages/BasePage.js
2025-03-06 10:56:42 +08:00

345 lines
10 KiB
JavaScript

/**
* 页面对象模型基类
* 提供所有页面共用的方法和属性
*/
class BasePage {
/**
* 创建页面对象
* @param {import('@playwright/test').Page} page Playwright页面对象
*/
constructor(page) {
this.page = page;
this.initializeSelectors();
this.initializeConfig();
}
/**
* 初始化选择器
* @protected
*/
initializeSelectors() {
this.selectors = {
// 通用加载状态
loadingMask: '.el-loading-mask',
// 通用错误提示
errorBox: '.el-message-box__message',
errorMessage: '.el-message--error',
// 通用对话框
dialog: '.el-dialog',
dialogTitle: '.el-dialog__title',
dialogClose: '.el-dialog__close',
// 通用按钮
submitButton: '.el-button--primary',
cancelButton: '.el-button--default',
// 通用表单元素
input: '.el-input__inner',
select: '.el-select',
checkbox: '.el-checkbox__input',
radio: '.el-radio__input',
// 通用消息提示
message: '.el-message',
// 通用分页
pagination: '.el-pagination',
// 通用表格
table: '.el-table',
tableHeader: '.el-table__header',
tableBody: '.el-table__body',
// 临时元素
temporaryElements: '.el-loading-mask, .el-message, .el-message-box'
};
}
/**
* 初始化配置
* @protected
*/
initializeConfig() {
this.config = {
timeout: 30000, // 默认超时时间
pageLoad: {
maxRetries: 30,
retryInterval: 500,
stabilityDelay: 1000
},
menuTimeout: parseInt(process.env.MENU_TIME_OUT || '30000', 10)
};
}
/**
* 导航到指定URL
* @param {string} url 目标URL
*/
async navigate(url) {
await this.page.goto(url, {
waitUntil: 'networkidle',
timeout: 30000
});
}
/**
* 等待元素可见
* @param {string} selector 元素选择器
* @param {Object} options 选项
* @param {boolean} [options.returnBoolean=false] 是否返回布尔值而不是元素
* @param {boolean} [options.firstOnly=false] 是否只获取第一个元素
* @returns {Promise<import('@playwright/test').Locator|boolean>} 元素定位器或是否可见
*/
async waitForElement(selector, options = {}) {
try {
const element = options.firstOnly ?
this.page.locator(selector).first() :
this.page.locator(selector);
await element.waitFor({
state: 'visible',
timeout: options.timeout || this.config.timeout
});
return options.returnBoolean ? true : element;
} catch (error) {
if (options.returnBoolean) {
return false;
}
throw error;
}
}
/**
* 点击元素
* @param {string} selector 元素选择器
* @param {Object} options 选项
*/
async click(selector, options = {}) {
const element = await this.waitForElement(selector, options);
await element.click(options);
}
/**
* 填写表单字段
* @param {string} selector 元素选择器
* @param {string} value 要填写的值
* @param {Object} options 选项
*/
async fill(selector, value, options = {}) {
const element = await this.waitForElement(selector, options);
await element.fill(value);
}
/**
* 获取元素文本
* @param {string} selector 元素选择器
* @param {Object} options 选项
* @returns {Promise<string>} 元素文本
*/
async getText(selector, options = {}) {
const element = await this.waitForElement(selector, options);
return element.textContent();
}
/**
* 检查元素是否可见
* @param {string} selector 元素选择器
* @param {Object} options 选项
* @returns {Promise<boolean>} 元素是否可见
*/
async isVisible(selector, options = {}) {
try {
await this.waitForElement(selector, {
timeout: options.timeout || 1000
});
return true;
} catch (error) {
return false;
}
}
/**
* 等待页面加载完成
* @deprecated 请使用 waitForPageLoadWithRetry 方法替代
*/
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle', {
timeout: 30000
});
}
/**
* 等待页面加载完成,带重试机制
* @param {Object} context 上下文信息对象
* @param {string} [context.text] 页面名称或描述
* @param {string} [context.path] 页面路径
* @param {string} [subContext] 子上下文信息(可选)
* @returns {Promise<boolean>} 页面是否加载成功
*/
async waitForPageLoadWithRetry(context, subContext = '') {
const pageName = this.getContextName(context, subContext);
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 {Object} context 上下文信息对象
* @param {string} [subContext] 子上下文信息
* @returns {string} 格式化的上下文名称
* @private
*/
getContextName(context, subContext = '') {
if (typeof context === 'string') {
return context;
}
const mainText = context.text || context.path || 'Unknown';
return subContext ? `${mainText} > ${subContext}` : mainText;
}
/**
* 获取页面标题
* @returns {Promise<string>} 页面标题
*/
async getTitle() {
return this.page.title();
}
/**
* 获取当前URL
* @returns {Promise<string>} 当前URL
*/
async getCurrentUrl() {
return this.page.url();
}
/**
* 截取页面截图
* @param {string} name 截图名称
* @param {Object} options 截图选项
*/
async takeScreenshot(name, options = {}) {
const screenshotPath = options.path || `./screenshots/${name}_${Date.now()}.png`;
await this.page.screenshot({
path: screenshotPath,
fullPage: options.fullPage || false,
...options
});
return screenshotPath;
}
/**
* 选择下拉菜单选项
* @param {string} selector 下拉菜单选择器
* @param {string} value 要选择的值
*/
async selectOption(selector, value) {
const element = await this.waitForElement(selector);
await element.selectOption(value);
}
/**
* 获取元素属性值
* @param {string} selector 元素选择器
* @param {string} attributeName 属性名
* @returns {Promise<string>} 属性值
*/
async getAttribute(selector, attributeName) {
const element = await this.waitForElement(selector);
return element.getAttribute(attributeName);
}
/**
* 安全点击元素
* @param {Object} element Playwright元素对象
* @returns {Promise<boolean>} 是否点击成功
*/
async safeClick(element) {
try {
await element.click();
return true;
} catch (error) {
console.error(`点击失败:`, error.message);
return false;
}
}
/**
* 等待指定时间
* @param {number} ms 等待时间(毫秒)
*/
async wait(ms) {
await this.page.waitForTimeout(ms);
}
/**
* 检查元素是否存在
* @param {string} selector 元素选择器
* @returns {Promise<boolean>} 是否存在
*/
async elementExists(selector) {
const count = await this.page.locator(selector).count();
return count > 0;
}
/**
* 获取元素文本
* @param {Object} element Playwright元素对象
* @returns {Promise<string>} 元素文本
*/
async getElementText(element) {
try {
const text = await element.textContent();
return text.trim();
} catch (error) {
console.error('获取元素文本失败:', error.message);
return '';
}
}
}
module.exports = BasePage;