feat: 添加管理后台页面和功能,优化测试和性能配置

refactor: 重构页面导航和滚动逻辑,提升用户体验

test: 更新测试配置和用例,增加覆盖率和稳定性

perf: 优化性能指标和阈值,适应开发环境需求

ci: 添加Lighthouse CI工作流,集成性能测试

docs: 更新API文档和健康检查端点

fix: 修复登录页面和表单提交问题

style: 调整响应式布局和可访问性改进

chore: 更新依赖项和脚本配置
This commit is contained in:
张翔
2026-03-24 10:11:30 +08:00
parent 08978d38c8
commit f5dec95a83
85 changed files with 12331 additions and 1408 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ export class TestDataGenerator {
}
static generateSpecialCharacters(): string {
return '!@#$%^&*()_+-=[]{}|;:,.<>?/~`';
return '!@#$%^*()_+-=[]{}|;:,.?/~`';
}
static generateChineseCharacters(): string {
+140
View File
@@ -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)));
}
}
}