feat: 完善BasePage页面对象
- 添加性能测量方法(measurePerformance, getCoreWebVitals) - 添加资源时序和网络时序测量方法 - 添加重试机制(retryOperation, waitForElementWithRetry等) - 添加日志记录功能 - 添加滚动和视口相关方法 - 添加元素状态检查方法 - 添加文件上传和对话框处理方法 - 优化截图目录自动创建
This commit is contained in:
+303
-1
@@ -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());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user