From 6270047221a79ab5475678ee4660ee08802f351d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Sat, 28 Feb 2026 15:18:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84BasePage=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加性能测量方法(measurePerformance, getCoreWebVitals) - 添加资源时序和网络时序测量方法 - 添加重试机制(retryOperation, waitForElementWithRetry等) - 添加日志记录功能 - 添加滚动和视口相关方法 - 添加元素状态检查方法 - 添加文件上传和对话框处理方法 - 优化截图目录自动创建 --- e2e/src/pages/BasePage.ts | 304 +++++++++++++++++++++++++++++++++++++- 1 file changed, 303 insertions(+), 1 deletion(-) diff --git a/e2e/src/pages/BasePage.ts b/e2e/src/pages/BasePage.ts index 2f7ba1e..65aeb91 100644 --- a/e2e/src/pages/BasePage.ts +++ b/e2e/src/pages/BasePage.ts @@ -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 { - 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 { @@ -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 { + 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( + operation: () => Promise, + maxRetries: number = 3, + delay: number = 1000 + ): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + await this.page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await this.waitForTimeout(500); + } + + async scrollToTop(): Promise { + 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 { + 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 { + 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 { + 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 { + const element = typeof locator === 'string' ? this.page.locator(locator) : locator; + await element.focus(); + } + + async blur(locator: Locator | string): Promise { + const element = typeof locator === 'string' ? this.page.locator(locator) : locator; + await element.blur(); + } + + async dragAndDrop(source: Locator | string, target: Locator | string): Promise { + 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 { + const element = typeof locator === 'string' ? this.page.locator(locator) : locator; + await element.setInputFiles(filePath); + } + + async clearInput(locator: Locator | string): Promise { + const element = typeof locator === 'string' ? this.page.locator(locator) : locator; + await element.clear(); + } + + async getInputValue(locator: Locator | string): Promise { + const element = typeof locator === 'string' ? this.page.locator(locator) : locator; + return await element.inputValue(); + } + + async selectText(locator: Locator | string): Promise { + const element = typeof locator === 'string' ? this.page.locator(locator) : locator; + await element.selectText(); + } + + async waitForFileDownload(downloadPromise: Promise): Promise { + const download = await downloadPromise; + const path = await download.path(); + return path || ''; + } + + async acceptDialog(): Promise { + this.page.on('dialog', (dialog) => dialog.accept()); + } + + async dismissDialog(): Promise { + this.page.on('dialog', (dialog) => dialog.dismiss()); + } + + async getDialogMessage(): Promise { + return new Promise((resolve) => { + this.page.on('dialog', (dialog) => { + resolve(dialog.message()); + }); + }); + } }