feat: 完善BasePage页面对象
- 添加性能测量方法(measurePerformance, getCoreWebVitals) - 添加资源时序和网络时序测量方法 - 添加重试机制(retryOperation, waitForElementWithRetry等) - 添加日志记录功能 - 添加滚动和视口相关方法 - 添加元素状态检查方法 - 添加文件上传和对话框处理方法 - 优化截图目录自动创建
This commit is contained in:
+303
-1
@@ -1,4 +1,6 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export class BasePage {
|
||||
readonly page: Page;
|
||||
@@ -46,7 +48,11 @@ export class BasePage {
|
||||
}
|
||||
|
||||
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> {
|
||||
@@ -148,4 +154,300 @@ export class BasePage {
|
||||
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
||||
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());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user