feat: 修复测试套件问题并添加Woodpecker CI配置
- 修复API测试认证问题:创建全局认证设置,更新Playwright配置 - 优化回归测试稳定性:增加超时时间到15秒,修复定位器 - 创建Woodpecker CI工作流:CI、部署和质量门禁配置 - 添加Jest配置和测试脚本 - 移除登录页面的默认账号密码显示(安全问题修复)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { getEnvironmentConfig } from './shared/config/environments';
|
||||
import { CustomReporter } from './shared/utils/reporting/CustomReporter';
|
||||
|
||||
const config = defineConfig({
|
||||
testDir: './dev-audit',
|
||||
|
||||
@@ -25,5 +25,5 @@ export const environments: Record<string, TestConfig> = {
|
||||
};
|
||||
|
||||
export function getEnvironmentConfig(env: string = 'development'): TestConfig {
|
||||
return environments[env] || environments.development;
|
||||
return environments[env] ?? environments.development!;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './environments';
|
||||
export * from './test-pages';
|
||||
export * from './test-data';
|
||||
export * from './base.config';
|
||||
@@ -66,7 +66,7 @@ export const testPages: Record<string, PageConfig> = {
|
||||
};
|
||||
|
||||
export function getPageConfig(pageKey: string): PageConfig {
|
||||
return testPages[pageKey] || testPages.home;
|
||||
return testPages[pageKey] ?? testPages.home!;
|
||||
}
|
||||
|
||||
export function getAllPageConfigs(): PageConfig[] {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test as base } from '@playwright/test';
|
||||
import { BasePage, HomePage, AboutPage, ContactPage, ProductsPage, ServicesPage, CasesPage, NewsPage } from '../pages';
|
||||
import { getEnvironmentConfig } from '../config/environments';
|
||||
import { TestConfig as CustomTestConfig } from '../types';
|
||||
|
||||
type MyFixtures = {
|
||||
basePage: BasePage;
|
||||
@@ -11,52 +12,52 @@ type MyFixtures = {
|
||||
servicesPage: ServicesPage;
|
||||
casesPage: CasesPage;
|
||||
newsPage: NewsPage;
|
||||
config: any;
|
||||
config: CustomTestConfig;
|
||||
};
|
||||
|
||||
export const test = base.extend<MyFixtures>({
|
||||
config: async ({}, use) => {
|
||||
config: async ({}, use: (value: CustomTestConfig) => Promise<void>) => {
|
||||
const env = process.env.TEST_ENV || 'development';
|
||||
const config = getEnvironmentConfig(env);
|
||||
await use(config);
|
||||
},
|
||||
|
||||
basePage: async ({ page }, use) => {
|
||||
basePage: async ({ page }, use: (value: BasePage) => Promise<void>) => {
|
||||
const basePage = new BasePage(page, '/');
|
||||
await use(basePage);
|
||||
},
|
||||
|
||||
homePage: async ({ page, config }, use) => {
|
||||
homePage: async ({ page, config }, use: (value: HomePage) => Promise<void>) => {
|
||||
const homePage = new HomePage(page, config);
|
||||
await use(homePage);
|
||||
},
|
||||
|
||||
aboutPage: async ({ page, config }, use) => {
|
||||
aboutPage: async ({ page, config }, use: (value: AboutPage) => Promise<void>) => {
|
||||
const aboutPage = new AboutPage(page, config);
|
||||
await use(aboutPage);
|
||||
},
|
||||
|
||||
contactPage: async ({ page, config }, use) => {
|
||||
contactPage: async ({ page, config }, use: (value: ContactPage) => Promise<void>) => {
|
||||
const contactPage = new ContactPage(page, config);
|
||||
await use(contactPage);
|
||||
},
|
||||
|
||||
productsPage: async ({ page, config }, use) => {
|
||||
productsPage: async ({ page, config }, use: (value: ProductsPage) => Promise<void>) => {
|
||||
const productsPage = new ProductsPage(page, config);
|
||||
await use(productsPage);
|
||||
},
|
||||
|
||||
servicesPage: async ({ page, config }, use) => {
|
||||
servicesPage: async ({ page, config }, use: (value: ServicesPage) => Promise<void>) => {
|
||||
const servicesPage = new ServicesPage(page, config);
|
||||
await use(servicesPage);
|
||||
},
|
||||
|
||||
casesPage: async ({ page, config }, use) => {
|
||||
casesPage: async ({ page, config }, use: (value: CasesPage) => Promise<void>) => {
|
||||
const casesPage = new CasesPage(page, config);
|
||||
await use(casesPage);
|
||||
},
|
||||
|
||||
newsPage: async ({ page, config }, use) => {
|
||||
newsPage: async ({ page, config }, use: (value: NewsPage) => Promise<void>) => {
|
||||
const newsPage = new NewsPage(page, config);
|
||||
await use(newsPage);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './base.fixture';
|
||||
@@ -2,8 +2,3 @@ export * from './config';
|
||||
export * from './pages';
|
||||
export * from './types';
|
||||
export * from './fixtures';
|
||||
export * from './utils/performance/PerformanceMonitor';
|
||||
export * from './utils/performance/LighthouseRunner';
|
||||
export * from './utils/performance/CoreWebVitals';
|
||||
export * from './utils/accessibility/AccessibilityTester';
|
||||
export * from './utils/seo/SEOValidator';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { getPageConfig } from '../config/test-pages';
|
||||
import { TestConfig } from '../types';
|
||||
|
||||
export class AboutPage extends BasePage {
|
||||
constructor(page: Page, config?) {
|
||||
constructor(page: Page, config?: TestConfig) {
|
||||
const pageConfig = getPageConfig('about');
|
||||
super(page, pageConfig.url, config);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { getPageConfig } from '../config/test-pages';
|
||||
import { TestConfig } from '../types';
|
||||
|
||||
export class CasesPage extends BasePage {
|
||||
constructor(page: Page, config?) {
|
||||
constructor(page: Page, config?: TestConfig) {
|
||||
const pageConfig = getPageConfig('cases');
|
||||
super(page, pageConfig.url, config);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { getPageConfig } from '../config/test-pages';
|
||||
import { TestConfig } from '../types';
|
||||
|
||||
export class ContactPage extends BasePage {
|
||||
constructor(page: Page, config?) {
|
||||
constructor(page: Page, config?: TestConfig) {
|
||||
const pageConfig = getPageConfig('contact');
|
||||
super(page, pageConfig.url, config);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { getPageConfig } from '../config/test-pages';
|
||||
import { TestConfig } from '../types';
|
||||
|
||||
export class HomePage extends BasePage {
|
||||
constructor(page: Page, config?) {
|
||||
constructor(page: Page, config?: TestConfig) {
|
||||
const pageConfig = getPageConfig('home');
|
||||
super(page, pageConfig.url, config);
|
||||
this.pageConfig = pageConfig;
|
||||
}
|
||||
|
||||
private pageConfig;
|
||||
|
||||
async getHeroTitle(): Promise<string> {
|
||||
return await this.getText('h1');
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { getPageConfig } from '../config/test-pages';
|
||||
import { TestConfig } from '../types';
|
||||
|
||||
export class NewsPage extends BasePage {
|
||||
constructor(page: Page, config?) {
|
||||
constructor(page: Page, config?: TestConfig) {
|
||||
const pageConfig = getPageConfig('news');
|
||||
super(page, pageConfig.url, config);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { getPageConfig } from '../config/test-pages';
|
||||
import { TestConfig } from '../types';
|
||||
|
||||
export class ProductsPage extends BasePage {
|
||||
constructor(page: Page, config?) {
|
||||
constructor(page: Page, config?: TestConfig) {
|
||||
const pageConfig = getPageConfig('products');
|
||||
super(page, pageConfig.url, config);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { getPageConfig } from '../config/test-pages';
|
||||
import { TestConfig } from '../types';
|
||||
|
||||
export class ServicesPage extends BasePage {
|
||||
constructor(page: Page, config?) {
|
||||
constructor(page: Page, config?: TestConfig) {
|
||||
const pageConfig = getPageConfig('services');
|
||||
super(page, pageConfig.url, config);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
export interface PageConfig {
|
||||
name: string;
|
||||
url: string;
|
||||
|
||||
@@ -28,6 +28,12 @@ export interface PerformanceMetrics {
|
||||
firstInputDelay: number;
|
||||
}
|
||||
|
||||
export interface PerformanceBaseline {
|
||||
timestamp: number;
|
||||
metrics: PerformanceMetrics;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ComparisonResult {
|
||||
status: 'regression' | 'improvement' | 'stable' | 'no-baseline';
|
||||
difference: number;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user