feat: 添加管理后台页面和功能,优化测试和性能配置
refactor: 重构页面导航和滚动逻辑,提升用户体验 test: 更新测试配置和用例,增加覆盖率和稳定性 perf: 优化性能指标和阈值,适应开发环境需求 ci: 添加Lighthouse CI工作流,集成性能测试 docs: 更新API文档和健康检查端点 fix: 修复登录页面和表单提交问题 style: 调整响应式布局和可访问性改进 chore: 更新依赖项和脚本配置
This commit is contained in:
@@ -101,7 +101,7 @@ export class TestDataGenerator {
|
||||
}
|
||||
|
||||
static generateSpecialCharacters(): string {
|
||||
return '!@#$%^&*()_+-=[]{}|;:,.<>?/~`';
|
||||
return '!@#$%^*()_+-=[]{}|;:,.?/~`';
|
||||
}
|
||||
|
||||
static generateChineseCharacters(): string {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
export class SmartWait {
|
||||
private page: Page;
|
||||
private defaultTimeout: number = 10000;
|
||||
private pollInterval: number = 100;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async waitForElement(locator: Locator, options?: { timeout?: number; state?: 'visible' | 'attached' | 'hidden' | 'detached' }) {
|
||||
const timeout = options?.timeout || this.defaultTimeout;
|
||||
const state = options?.state || 'visible';
|
||||
|
||||
try {
|
||||
await locator.waitFor({ state, timeout });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(`等待元素超时: ${timeout}ms, state: ${state}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForNetworkIdle(timeout: number = 5000) {
|
||||
try {
|
||||
await this.page.waitForLoadState('networkidle', { timeout });
|
||||
} catch (error) {
|
||||
console.log(`等待网络空闲超时: ${timeout}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
async waitForStableElement(locator: Locator, options?: { timeout?: number; stableDuration?: number }) {
|
||||
const timeout = options?.timeout || this.defaultTimeout;
|
||||
const stableDuration = options?.stableDuration || 500;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
const isVisible = await locator.isVisible();
|
||||
if (isVisible) {
|
||||
const boundingBox = await locator.boundingBox();
|
||||
await this.page.waitForTimeout(stableDuration);
|
||||
|
||||
const newBoundingBox = await locator.boundingBox();
|
||||
if (boundingBox && newBoundingBox &&
|
||||
Math.abs(boundingBox.x - newBoundingBox.x) < 2 &&
|
||||
Math.abs(boundingBox.y - newBoundingBox.y) < 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(this.pollInterval);
|
||||
}
|
||||
|
||||
throw new Error(`元素未在 ${timeout}ms 内稳定`);
|
||||
}
|
||||
|
||||
async waitForTextContent(locator: Locator, expectedText: string | RegExp, options?: { timeout?: number }) {
|
||||
const timeout = options?.timeout || this.defaultTimeout;
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
const text = await locator.textContent();
|
||||
if (text) {
|
||||
if (typeof expectedText === 'string') {
|
||||
if (text.includes(expectedText)) {
|
||||
return true;
|
||||
}
|
||||
} else if (expectedText instanceof RegExp) {
|
||||
if (expectedText.test(text)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(this.pollInterval);
|
||||
}
|
||||
|
||||
throw new Error(`文本内容未在 ${timeout}ms 内出现: ${expectedText}`);
|
||||
}
|
||||
|
||||
async waitForPageReady(timeout: number = 15000) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await this.page.waitForLoadState('domcontentloaded', { timeout });
|
||||
|
||||
await this.waitForNetworkIdle(3000);
|
||||
|
||||
const body = this.page.locator('body');
|
||||
await this.waitForElement(body, { timeout: 5000, state: 'visible' });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(`页面未在 ${timeout}ms 内就绪`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async retry<T>(
|
||||
fn: () => Promise<T>,
|
||||
options?: { maxRetries?: number; delay?: number; onRetry?: (error: Error, attempt: number) => void }
|
||||
): Promise<T> {
|
||||
const maxRetries = options?.maxRetries || 3;
|
||||
const delay = options?.delay || 1000;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
if (options?.onRetry) {
|
||||
options.onRetry(lastError, attempt);
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`重试 ${attempt}/${maxRetries}: ${lastError.message}`);
|
||||
await this.page.waitForTimeout(delay * attempt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('重试次数耗尽');
|
||||
}
|
||||
|
||||
async waitForAnimationFrame(count: number = 2) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
await this.page.evaluate(() => new Promise(resolve => requestAnimationFrame(resolve)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user