feat: 修复测试套件问题并添加Woodpecker CI配置

- 修复API测试认证问题:创建全局认证设置,更新Playwright配置
- 优化回归测试稳定性:增加超时时间到15秒,修复定位器
- 创建Woodpecker CI工作流:CI、部署和质量门禁配置
- 添加Jest配置和测试脚本
- 移除登录页面的默认账号密码显示(安全问题修复)
This commit is contained in:
张翔
2026-03-09 10:26:02 +08:00
parent 96c96fe75d
commit 6d92024b63
68 changed files with 5584 additions and 167 deletions
@@ -1,16 +1,20 @@
import { Page } from '@playwright/test';
import { CoreWebVitals } from '../../types';
import { CoreWebVitals as CoreWebVitalsMetrics } from '../../types';
export class CoreWebVitals {
constructor(private page: Page) {}
async measureLCP(): Promise<number> {
return await this.page.evaluate(() => {
return new Promise((resolve) => {
return new Promise<number>((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
resolve(lastEntry.startTime);
if (lastEntry) {
resolve(lastEntry.startTime);
} else {
resolve(0);
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
});
});
@@ -18,11 +22,15 @@ export class CoreWebVitals {
async measureFID(): Promise<number> {
return await this.page.evaluate(() => {
return new Promise((resolve) => {
return new Promise<number>((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const firstEntry = entries[0];
resolve(firstEntry.processingStart - firstEntry.startTime);
const firstEntry = entries[0] as any;
if (firstEntry) {
resolve(firstEntry.processingStart - firstEntry.startTime);
} else {
resolve(0);
}
}).observe({ type: 'first-input', buffered: true });
});
});
@@ -30,12 +38,12 @@ export class CoreWebVitals {
async measureCLS(): Promise<number> {
return await this.page.evaluate(() => {
return new Promise((resolve) => {
return new Promise<number>((resolve) => {
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
const value = entry.value;
if (!(entry as any).hadRecentInput) {
const value = (entry as any).value;
clsValue = Math.max(clsValue, value);
}
}
@@ -46,7 +54,7 @@ export class CoreWebVitals {
});
}
async measureAll(): Promise<CoreWebVitals> {
async measureAll(): Promise<CoreWebVitalsMetrics> {
const [lcp, fid, cls] = await Promise.all([
this.measureLCP(),
this.measureFID(),
@@ -69,12 +77,14 @@ export class CoreWebVitals {
async measureFCP(): Promise<number> {
return await this.page.evaluate(() => {
return new Promise((resolve) => {
return new Promise<number>((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const fcpEntry = entries.find((entry: any) => entry.name === 'first-contentful-paint');
if (fcpEntry) {
resolve(fcpEntry.startTime);
} else {
resolve(0);
}
}).observe({ type: 'paint', buffered: true });
});
@@ -6,7 +6,7 @@ export class LighthouseRunner {
async runLighthouse(url: string): Promise<LighthouseResult> {
const results = await this.page.evaluate(async () => {
return new Promise((resolve) => {
return new Promise<LighthouseResult>((resolve) => {
if (!(window as any).lighthouse) {
resolve({
performance: 0,
@@ -46,7 +46,16 @@ export class LighthouseRunner {
};
}> {
const results = await this.page.evaluate(async () => {
return new Promise((resolve) => {
return new Promise<{
score: number;
metrics: {
firstContentfulPaint: number;
largestContentfulPaint: number;
cumulativeLayoutShift: number;
firstInputDelay: number;
speedIndex: number;
};
}>((resolve) => {
if (!(window as any).lighthouse) {
resolve({
score: 0,
@@ -61,18 +70,17 @@ export class LighthouseRunner {
return;
}
(window as any).lighthouse(location.href, {
(window as any).lighthouse(window.location.href, {
onlyCategories: ['performance']
}).then((result: any) => {
const audits = result.audits;
resolve({
score: Math.round(result.categories.performance.score * 100),
metrics: {
firstContentfulPaint: audits['first-contentful-paint'].numericValue,
largestContentfulPaint: audits['largest-contentful-paint'].numericValue,
cumulativeLayoutShift: audits['cumulative-layout-shift'].numericValue,
firstInputDelay: audits['max-potential-fid'].numericValue,
speedIndex: audits['speed-index'].numericValue
firstContentfulPaint: result.audits['first-contentful-paint'].numericValue,
largestContentfulPaint: result.audits['largest-contentful-paint'].numericValue,
cumulativeLayoutShift: result.audits['cumulative-layout-shift'].numericValue,
firstInputDelay: result.audits['max-potential-fid'].numericValue,
speedIndex: result.audits['speed-index'].numericValue
}
});
});
@@ -6,7 +6,7 @@ export class CustomReporter {
private results: any[] = [];
private startTime: number = Date.now();
onBegin(config: any, suite: Suite) {
onBegin(_config: any, suite: Suite) {
console.log('\n=== 测试执行开始 ===');
console.log(`测试套件: ${suite.allTests().length} 个测试`);
}
@@ -38,7 +38,7 @@ export class CustomReporter {
return 'form';
}
private generateCustomReport(result: FullResult, duration: number): string {
private generateCustomReport(_result: FullResult, duration: number): string {
const passed = this.results.filter(r => r.status === 'passed').length;
const failed = this.results.filter(r => r.status === 'failed').length;
const passRate = ((passed / this.results.length) * 100).toFixed(2);
@@ -1,15 +1,28 @@
import { TestResult, PerformanceMetrics, ComparisonResult } from '../../types/reporting';
import { TestResult, PerformanceMetrics, ComparisonResult, PerformanceBaseline as PerformanceBaselineType } from '../../types/reporting';
export class PerformanceBaseline {
private baseline: Map<string, PerformanceMetrics> = new Map();
calculate(results: TestResult[]): PerformanceBaseline {
calculate(results: TestResult[]): PerformanceBaselineType {
results.forEach(result => {
if (result.type === 'performance' && result.metrics) {
this.updateBaseline(result);
}
});
return this;
const firstBaseline = this.baseline.values().next().value;
return {
timestamp: Date.now(),
metrics: firstBaseline || {
loadTime: 0,
domContentLoaded: 0,
firstContentfulPaint: 0,
largestContentfulPaint: 0,
cumulativeLayoutShift: 0,
firstInputDelay: 0
},
url: ''
};
}
private updateBaseline(result: TestResult): void {
@@ -113,7 +113,7 @@ export class SEOValidator {
};
}
private calculateScore(metaTags: MetaTagResult, headings: HeadingResult, links: LinkResult, images: ImageResult): number {
private calculateScore(metaTags: MetaTagResult, headings: HeadingResult, _links: LinkResult, images: ImageResult): number {
let score = 0;
let total = 0;
@@ -1,4 +1,4 @@
import { formData, performanceThresholds } from '../../config/test-data';
import { performanceThresholds } from '../../config/test-data';
export interface ContactFormData {
name: string;
@@ -19,7 +19,7 @@ export class TestDataVersion {
}
listVersions(): string[] {
return Array.from(new Set(Array.from(this.versions.keys()).map(k => k.split('-')[0])));
return Array.from(new Set(Array.from(this.versions.keys()).map(k => k.split('-')[0]).filter((v): v is string => v !== undefined)));
}
export(): string {