feat: 完善BasePage页面对象

- 添加性能测量方法(measurePerformance, getCoreWebVitals)
- 添加资源时序和网络时序测量方法
- 添加重试机制(retryOperation, waitForElementWithRetry等)
- 添加日志记录功能
- 添加滚动和视口相关方法
- 添加元素状态检查方法
- 添加文件上传和对话框处理方法
- 优化截图目录自动创建
This commit is contained in:
张翔
2026-02-28 15:18:53 +08:00
parent 925f79c45a
commit 6270047221
+303 -1
View File
@@ -1,4 +1,6 @@
import { Page, Locator } from '@playwright/test'; import { Page, Locator } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
export class BasePage { export class BasePage {
readonly page: Page; readonly page: Page;
@@ -46,7 +48,11 @@ export class BasePage {
} }
async takeScreenshot(filename: string): Promise<void> { async takeScreenshot(filename: string): Promise<void> {
await this.page.screenshot({ path: `test-results/screenshots/${filename}` }); const screenshotDir = 'test-results/screenshots';
if (!fs.existsSync(screenshotDir)) {
fs.mkdirSync(screenshotDir, { recursive: true });
}
await this.page.screenshot({ path: path.join(screenshotDir, filename) });
} }
async hover(locator: Locator | string): Promise<void> { async hover(locator: Locator | string): Promise<void> {
@@ -148,4 +154,300 @@ export class BasePage {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator; const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
return await element.isChecked(); return await element.isChecked();
} }
async measurePerformance(): Promise<{
loadTime: number;
domContentLoaded: number;
firstPaint: number;
firstContentfulPaint: number;
}> {
const metrics = await this.page.evaluate(() => {
const performance = window.performance;
const timing = performance.timing;
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
return {
loadTime: timing.loadEventEnd - timing.navigationStart,
domContentLoaded: timing.domContentLoadedEventEnd - timing.navigationStart,
firstPaint: navigation ? navigation.loadEventEnd - navigation.fetchStart : 0,
firstContentfulPaint: navigation ? navigation.domContentLoadedEventEnd - navigation.fetchStart : 0,
};
});
return metrics;
}
async getCoreWebVitals(): Promise<{
largestContentfulPaint: number;
firstInputDelay: number;
cumulativeLayoutShift: number;
}> {
const vitals = await this.page.evaluate(() => {
return new Promise((resolve) => {
const result = {
largestContentfulPaint: 0,
firstInputDelay: 0,
cumulativeLayoutShift: 0,
};
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.entryType === 'largest-contentful-paint') {
result.largestContentfulPaint = entry.startTime;
}
if (entry.entryType === 'first-input') {
result.firstInputDelay = (entry as PerformanceEventTiming).processingStart - entry.startTime;
}
if (entry.entryType === 'layout-shift') {
if (!(entry as any).hadRecentInput) {
result.cumulativeLayoutShift += (entry as any).value;
}
}
});
});
observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] });
setTimeout(() => {
observer.disconnect();
resolve(result);
}, 3000);
});
});
return vitals as any;
}
async getResourceTiming(): Promise<PerformanceResourceTiming[]> {
return await this.page.evaluate(() => {
return performance.getEntriesByType('resource') as PerformanceResourceTiming[];
});
}
async getNetworkTiming(): Promise<{
dns: number;
tcp: number;
ssl: number;
request: number;
response: number;
total: number;
}> {
return await this.page.evaluate(() => {
const timing = performance.timing;
return {
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
ssl: timing.connectEnd - timing.secureConnectionStart,
request: timing.responseStart - timing.requestStart,
response: timing.responseEnd - timing.responseStart,
total: timing.loadEventEnd - timing.navigationStart,
};
});
}
async retryOperation<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
let lastError: Error | undefined;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (i < maxRetries - 1) {
await this.waitForTimeout(delay);
}
}
}
throw lastError;
}
async log(message: string, level: 'info' | 'warn' | 'error' = 'info'): Promise<void> {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
switch (level) {
case 'warn':
console.warn(logMessage);
break;
case 'error':
console.error(logMessage);
break;
default:
console.log(logMessage);
}
}
async waitForElementWithRetry(
locator: Locator | string,
options?: { timeout?: number; retries?: number }
): Promise<void> {
const timeout = options?.timeout || 5000;
const retries = options?.retries || 3;
await this.retryOperation(
async () => {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
await element.waitFor({ state: 'visible', timeout });
},
retries,
1000
);
}
async clickWithRetry(
locator: Locator | string,
options?: { timeout?: number; retries?: number }
): Promise<void> {
const retries = options?.retries || 3;
await this.retryOperation(
async () => {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
await element.click();
},
retries,
1000
);
}
async fillWithRetry(
locator: Locator | string,
value: string,
options?: { timeout?: number; retries?: number }
): Promise<void> {
const retries = options?.retries || 3;
await this.retryOperation(
async () => {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
await element.fill(value);
},
retries,
1000
);
}
async scrollToEnd(): Promise<void> {
await this.page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
await this.waitForTimeout(500);
}
async scrollToTop(): Promise<void> {
await this.page.evaluate(() => {
window.scrollTo(0, 0);
});
await this.waitForTimeout(500);
}
async getScrollPosition(): Promise<{ x: number; y: number }> {
return await this.page.evaluate(() => {
return {
x: window.scrollX,
y: window.scrollY,
};
});
}
async isElementInViewport(locator: Locator | string): Promise<boolean> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
return await element.evaluate((el) => {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
});
}
async getElementBoundingBox(locator: Locator | string): Promise<{
x: number;
y: number;
width: number;
height: number;
} | null> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
return await element.boundingBox();
}
async getElementStyle(locator: Locator | string, property: string): Promise<string> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
return await element.evaluate((el, prop) => {
return window.getComputedStyle(el).getPropertyValue(prop);
}, property);
}
async isElementFocused(locator: Locator | string): Promise<boolean> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
return await element.evaluate((el) => {
return document.activeElement === el;
});
}
async focus(locator: Locator | string): Promise<void> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
await element.focus();
}
async blur(locator: Locator | string): Promise<void> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
await element.blur();
}
async dragAndDrop(source: Locator | string, target: Locator | string): Promise<void> {
const sourceElement = typeof source === 'string' ? this.page.locator(source) : source;
const targetElement = typeof target === 'string' ? this.page.locator(target) : target;
await sourceElement.dragTo(targetElement);
}
async uploadFile(locator: Locator | string, filePath: string): Promise<void> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
await element.setInputFiles(filePath);
}
async clearInput(locator: Locator | string): Promise<void> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
await element.clear();
}
async getInputValue(locator: Locator | string): Promise<string> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
return await element.inputValue();
}
async selectText(locator: Locator | string): Promise<void> {
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
await element.selectText();
}
async waitForFileDownload(downloadPromise: Promise<any>): Promise<string> {
const download = await downloadPromise;
const path = await download.path();
return path || '';
}
async acceptDialog(): Promise<void> {
this.page.on('dialog', (dialog) => dialog.accept());
}
async dismissDialog(): Promise<void> {
this.page.on('dialog', (dialog) => dialog.dismiss());
}
async getDialogMessage(): Promise<string> {
return new Promise((resolve) => {
this.page.on('dialog', (dialog) => {
resolve(dialog.message());
});
});
}
} }