08ea5fbe98
添加用户管理视图、API和状态管理文件
285 lines
6.9 KiB
TypeScript
285 lines
6.9 KiB
TypeScript
/**
|
|
* 基础页面类
|
|
* 所有页面对象的基类,提供通用的页面操作方法
|
|
*/
|
|
|
|
import { Page, Locator, expect } from '@playwright/test';
|
|
import { testConfig } from '../config/test-config';
|
|
import { testLogger } from '../utils/test-logger';
|
|
|
|
export interface PageOptions {
|
|
baseURL?: string;
|
|
timeout?: number;
|
|
}
|
|
|
|
export class BasePage {
|
|
protected page: Page;
|
|
protected baseURL: string;
|
|
protected defaultTimeout: number;
|
|
|
|
constructor(page: Page, options: PageOptions = {}) {
|
|
this.page = page;
|
|
this.baseURL = options.baseURL || testConfig.getConfig().baseURL;
|
|
this.defaultTimeout = options.timeout || testConfig.getTimeout('default');
|
|
}
|
|
|
|
/**
|
|
* 导航到指定路径
|
|
*/
|
|
async navigate(path: string): Promise<void> {
|
|
testLogger.debug(`导航到: ${this.baseURL}${path}`);
|
|
await this.page.goto(`${this.baseURL}${path}`, {
|
|
timeout: testConfig.getTimeout('navigation'),
|
|
});
|
|
await this.waitForLoad();
|
|
}
|
|
|
|
/**
|
|
* 等待页面加载完成
|
|
*/
|
|
async waitForLoad(): Promise<void> {
|
|
await this.page.waitForLoadState('networkidle', {
|
|
timeout: this.defaultTimeout,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 等待元素可见
|
|
*/
|
|
async waitForVisible(selector: string, timeout?: number): Promise<Locator> {
|
|
const locator = this.page.locator(selector);
|
|
await locator.waitFor({
|
|
state: 'visible',
|
|
timeout: timeout || this.defaultTimeout,
|
|
});
|
|
return locator;
|
|
}
|
|
|
|
/**
|
|
* 等待元素隐藏
|
|
*/
|
|
async waitForHidden(selector: string, timeout?: number): Promise<void> {
|
|
const locator = this.page.locator(selector);
|
|
await locator.waitFor({
|
|
state: 'hidden',
|
|
timeout: timeout || this.defaultTimeout,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 点击元素
|
|
*/
|
|
async clickElement(selector: string, options?: { force?: boolean }): Promise<void> {
|
|
testLogger.debug(`点击元素: ${selector}`);
|
|
const locator = await this.waitForVisible(selector);
|
|
await locator.click({ force: options?.force });
|
|
}
|
|
|
|
/**
|
|
* 填写输入框
|
|
*/
|
|
async fillInput(selector: string, value: string, options?: { clear?: boolean }): Promise<void> {
|
|
testLogger.debug(`填写输入框: ${selector} = ${value}`);
|
|
const locator = await this.waitForVisible(selector);
|
|
|
|
if (options?.clear !== false) {
|
|
await locator.clear();
|
|
}
|
|
|
|
await locator.fill(value);
|
|
}
|
|
|
|
/**
|
|
* 获取元素文本
|
|
*/
|
|
async getElementText(selector: string): Promise<string> {
|
|
const locator = await this.waitForVisible(selector);
|
|
return await locator.textContent() || '';
|
|
}
|
|
|
|
/**
|
|
* 检查元素是否存在
|
|
*/
|
|
async elementExists(selector: string): Promise<boolean> {
|
|
const locator = this.page.locator(selector);
|
|
return await locator.count() > 0;
|
|
}
|
|
|
|
/**
|
|
* 检查元素是否可见
|
|
*/
|
|
async isElementVisible(selector: string): Promise<boolean> {
|
|
const locator = this.page.locator(selector);
|
|
return await locator.isVisible().catch(() => false);
|
|
}
|
|
|
|
/**
|
|
* 获取页面标题
|
|
*/
|
|
async getPageTitle(): Promise<string> {
|
|
return await this.page.title();
|
|
}
|
|
|
|
/**
|
|
* 获取当前URL
|
|
*/
|
|
async getCurrentURL(): Promise<string> {
|
|
return this.page.url();
|
|
}
|
|
|
|
/**
|
|
* 等待URL变化
|
|
*/
|
|
async waitForURL(pattern: string | RegExp, timeout?: number): Promise<void> {
|
|
await this.page.waitForURL(pattern, {
|
|
timeout: timeout || testConfig.getTimeout('navigation'),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 截图
|
|
*/
|
|
async takeScreenshot(name: string, fullPage: boolean = true): Promise<string> {
|
|
const path = `test-results/screenshots/${name}.png`;
|
|
await this.page.screenshot({ path, fullPage });
|
|
testLogger.addScreenshot(path);
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* 滚动到元素
|
|
*/
|
|
async scrollToElement(selector: string): Promise<void> {
|
|
const locator = this.page.locator(selector);
|
|
await locator.scrollIntoViewIfNeeded();
|
|
}
|
|
|
|
/**
|
|
* 滚动页面
|
|
*/
|
|
async scrollPage(x: number, y: number): Promise<void> {
|
|
await this.page.evaluate(([scrollX, scrollY]) => {
|
|
window.scrollTo(scrollX, scrollY);
|
|
}, [x, y]);
|
|
}
|
|
|
|
/**
|
|
* 等待指定时间
|
|
*/
|
|
async waitForTimeout(ms: number): Promise<void> {
|
|
await this.page.waitForTimeout(ms);
|
|
}
|
|
|
|
/**
|
|
* 选择下拉框选项
|
|
*/
|
|
async selectOption(selector: string, value: string): Promise<void> {
|
|
const locator = this.page.locator(selector);
|
|
await locator.selectOption(value);
|
|
}
|
|
|
|
/**
|
|
* 检查复选框
|
|
*/
|
|
async checkCheckbox(selector: string): Promise<void> {
|
|
const locator = this.page.locator(selector);
|
|
await locator.check();
|
|
}
|
|
|
|
/**
|
|
* 取消复选框
|
|
*/
|
|
async uncheckCheckbox(selector: string): Promise<void> {
|
|
const locator = this.page.locator(selector);
|
|
await locator.uncheck();
|
|
}
|
|
|
|
/**
|
|
* 获取元素数量
|
|
*/
|
|
async getElementCount(selector: string): Promise<number> {
|
|
return await this.page.locator(selector).count();
|
|
}
|
|
|
|
/**
|
|
* 悬停在元素上
|
|
*/
|
|
async hoverElement(selector: string): Promise<void> {
|
|
const locator = await this.waitForVisible(selector);
|
|
await locator.hover();
|
|
}
|
|
|
|
/**
|
|
* 拖拽元素
|
|
*/
|
|
async dragElement(sourceSelector: string, targetSelector: string): Promise<void> {
|
|
const source = this.page.locator(sourceSelector);
|
|
const target = this.page.locator(targetSelector);
|
|
await source.dragTo(target);
|
|
}
|
|
|
|
/**
|
|
* 刷新页面
|
|
*/
|
|
async reload(): Promise<void> {
|
|
await this.page.reload();
|
|
await this.waitForLoad();
|
|
}
|
|
|
|
/**
|
|
* 返回上一页
|
|
*/
|
|
async goBack(): Promise<void> {
|
|
await this.page.goBack();
|
|
await this.waitForLoad();
|
|
}
|
|
|
|
/**
|
|
* 前进到下一页
|
|
*/
|
|
async goForward(): Promise<void> {
|
|
await this.page.goForward();
|
|
await this.waitForLoad();
|
|
}
|
|
|
|
/**
|
|
* 执行键盘操作
|
|
*/
|
|
async pressKey(key: string): Promise<void> {
|
|
await this.page.keyboard.press(key);
|
|
}
|
|
|
|
/**
|
|
* 上传文件
|
|
*/
|
|
async uploadFile(selector: string, filePath: string): Promise<void> {
|
|
const locator = this.page.locator(selector);
|
|
await locator.setInputFiles(filePath);
|
|
}
|
|
|
|
/**
|
|
* 获取页面性能指标
|
|
*/
|
|
async getPerformanceMetrics(): Promise<Record<string, number>> {
|
|
return await this.page.evaluate(() => {
|
|
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
|
return {
|
|
loadTime: navigation.loadEventEnd - navigation.startTime,
|
|
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.startTime,
|
|
firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime || 0,
|
|
firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 验证页面加载性能
|
|
*/
|
|
async verifyPerformance(maxLoadTime: number = 3000): Promise<void> {
|
|
const metrics = await this.getPerformanceMetrics();
|
|
testLogger.info('页面性能指标', metrics);
|
|
|
|
expect(metrics.loadTime).toBeLessThan(maxLoadTime);
|
|
}
|
|
}
|