import { Page, Locator } from '@playwright/test'; import * as fs from 'fs'; import * as path from 'path'; export class BasePage { readonly page: Page; readonly mobileMenuButton: Locator; readonly mobileMenu: Locator; readonly mobileMenuCloseButton: Locator; constructor(page: Page) { this.page = page; this.mobileMenuButton = page.getByRole('button', { name: /打开菜单|menu/i }); this.mobileMenu = page.locator('[role="navigation"][aria-label="移动端导航"], #mobile-menu'); this.mobileMenuCloseButton = page.getByRole('button', { name: /关闭菜单|close/i }); } async navigate(url: string): Promise { await this.page.goto(url); } async waitForLoadState(state: 'load' | 'domcontentloaded' | 'networkidle' = 'load'): Promise { await this.page.waitForLoadState(state); } async click(locator: Locator | string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; await element.click(); } async fill(locator: Locator | string, value: string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; await element.fill(value); } async getText(locator: Locator | string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; return await element.textContent() || ''; } async isVisible(locator: Locator | string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; return await element.isVisible(); } async waitForElement(locator: Locator | string, timeout: number = 5000): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; await element.waitFor({ state: 'visible', timeout }); } async scrollToElement(locator: Locator | string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; await element.scrollIntoViewIfNeeded(); } async takeScreenshot(filename: string): Promise { 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 { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; await element.hover(); } async selectOption(locator: Locator | string, value: string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; await element.selectOption(value); } async check(locator: Locator | string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; await element.check(); } async uncheck(locator: Locator | string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; await element.uncheck(); } async waitForURL(url: string | RegExp, timeout: number = 5000): Promise { await this.page.waitForURL(url, { timeout }); } async getCurrentURL(): Promise { return this.page.url(); } async getTitle(): Promise { return await this.page.title(); } async waitForSelector(locator: Locator | string, options?: { state?: 'attached' | 'detached' | 'visible' | 'hidden', timeout?: number }): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; await element.waitFor(options); } async getAttribute(locator: Locator | string, attribute: string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; return await element.getAttribute(attribute); } async pressKey(key: string): Promise { await this.page.keyboard.press(key); } async type(locator: Locator | string, text: string, options?: { delay?: number }): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; await element.type(text, options); } async waitForNavigation(options?: { url?: string | RegExp, timeout?: number }): Promise { await this.page.waitForNavigation(options); } async reload(): Promise { await this.page.reload(); } async goBack(): Promise { await this.page.goBack(); } async goForward(): Promise { await this.page.goForward(); } async evaluate(pageFunction: () => T): Promise { return await this.page.evaluate(pageFunction); } async waitForTimeout(timeout: number): Promise { await this.page.waitForTimeout(timeout); } async count(locator: Locator | string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; return await element.count(); } async allTextContents(locator: Locator | string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; return await element.allTextContents(); } async isDisabled(locator: Locator | string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; return await element.isDisabled(); } async isEnabled(locator: Locator | string): Promise { const element = typeof locator === 'string' ? this.page.locator(locator) : locator; return await element.isEnabled(); } async isChecked(locator: Locator | string): Promise { 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 scrollToTop(): Promise { await this.page.evaluate(() => { window.scrollTo(0, 0); document.documentElement.scrollTop = 0; document.body.scrollTop = 0; }); await this.page.waitForTimeout(1000); } async scrollToBottom(): Promise { await this.page.evaluate(() => { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); }); await this.page.waitForTimeout(1000); } async scrollToElement(selector: string): Promise { const element = this.page.locator(selector); await element.scrollIntoViewIfNeeded({ timeout: 5000 }); await this.page.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()); }); }); } async openMobileMenu() { await this.mobileMenuButton.click(); await this.mobileMenu.waitFor({ state: 'visible', timeout: 5000 }); } async closeMobileMenu() { if (await this.mobileMenu.isVisible()) { await this.mobileMenuCloseButton.click(); await this.mobileMenu.waitFor({ state: 'hidden', timeout: 5000 }); } } async isMobileMenuOpen() { return await this.mobileMenu.isVisible(); } }