2353 lines
57 KiB
Markdown
2353 lines
57 KiB
Markdown
# 测试框架重构实施计划
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**目标:** 创建统一的测试框架,整合E2E测试和开发环境测试,提高代码复用性和可维护性
|
|
|
|
**架构:** 采用渐进式迁移策略,首先创建共享的基础层(页面对象、配置、工具类),然后迁移开发环境测试到TypeScript/Playwright,最后优化和清理
|
|
|
|
**Tech Stack:** Playwright, TypeScript, Node.js, Git Worktrees
|
|
|
|
---
|
|
|
|
## 阶段1:创建共享层
|
|
|
|
### Task 1: 创建共享层目录结构
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/config/`
|
|
- Create: `test-framework/shared/pages/`
|
|
- Create: `test-framework/shared/utils/`
|
|
- Create: `test-framework/shared/fixtures/`
|
|
- Create: `test-framework/shared/types/`
|
|
|
|
**Step 1: 创建共享层基础目录**
|
|
|
|
```bash
|
|
mkdir -p test-framework/shared/{config,pages,utils,fixtures,types}
|
|
mkdir -p test-framework/shared/utils/{performance,seo,accessibility,forms,mobile,reporting,common}
|
|
mkdir -p test-framework/e2e
|
|
mkdir -p test-framework/dev-audit/{performance,seo,accessibility,forms}
|
|
mkdir -p test-framework/reports/{html,json,screenshots}
|
|
```
|
|
|
|
**Step 2: 验证目录结构**
|
|
|
|
Run: `tree test-framework/ -L 3`
|
|
|
|
Expected: 所有目录创建成功
|
|
|
|
**Step 3: 提交**
|
|
|
|
```bash
|
|
git add test-framework/
|
|
git commit -m "feat: create shared layer directory structure"
|
|
```
|
|
|
|
### Task 2: 创建类型定义
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/types/page.types.ts`
|
|
- Create: `test-framework/shared/types/test.types.ts`
|
|
- Create: `test-framework/shared/types/performance.types.ts`
|
|
- Create: `test-framework/shared/types/accessibility.types.ts`
|
|
- Create: `test-framework/shared/types/seo.types.ts`
|
|
- Create: `test-framework/shared/types/index.ts`
|
|
|
|
**Step 1: 创建页面对象类型**
|
|
|
|
```typescript
|
|
// test-framework/shared/types/page.types.ts
|
|
import { Page, Locator } from '@playwright/test';
|
|
|
|
export interface PageConfig {
|
|
name: string;
|
|
url: string;
|
|
selectors: {
|
|
title: string;
|
|
[key: string]: string;
|
|
};
|
|
}
|
|
|
|
export interface PageSelectors {
|
|
[key: string]: string;
|
|
}
|
|
|
|
export interface NavigationItem {
|
|
name: string;
|
|
url: string;
|
|
selector: string;
|
|
}
|
|
```
|
|
|
|
**Step 2: 创建测试类型**
|
|
|
|
```typescript
|
|
// test-framework/shared/types/test.types.ts
|
|
export interface TestConfig {
|
|
baseURL: string;
|
|
timeout: number;
|
|
retries: number;
|
|
environment: string;
|
|
headless: boolean;
|
|
slowMo?: number;
|
|
}
|
|
|
|
export interface TestResult {
|
|
name: string;
|
|
status: 'passed' | 'failed' | 'skipped';
|
|
duration: number;
|
|
errors?: Error[];
|
|
}
|
|
|
|
export interface TestSuite {
|
|
name: string;
|
|
tests: TestResult[];
|
|
summary: {
|
|
total: number;
|
|
passed: number;
|
|
failed: number;
|
|
skipped: number;
|
|
duration: number;
|
|
};
|
|
}
|
|
```
|
|
|
|
**Step 3: 创建性能测试类型**
|
|
|
|
```typescript
|
|
// test-framework/shared/types/performance.types.ts
|
|
export interface PerformanceMetrics {
|
|
loadTime: number;
|
|
domContentLoaded: number;
|
|
firstPaint: number;
|
|
firstContentfulPaint: number;
|
|
}
|
|
|
|
export interface CoreWebVitals {
|
|
largestContentfulPaint: number;
|
|
firstInputDelay: number;
|
|
cumulativeLayoutShift: number;
|
|
}
|
|
|
|
export interface ResourceTiming {
|
|
name: string;
|
|
duration: number;
|
|
size: number;
|
|
type: string;
|
|
}
|
|
|
|
export interface NetworkTiming {
|
|
dns: number;
|
|
tcp: number;
|
|
ssl: number;
|
|
request: number;
|
|
response: number;
|
|
total: number;
|
|
}
|
|
|
|
export interface LighthouseResult {
|
|
performance: number;
|
|
accessibility: number;
|
|
bestPractices: number;
|
|
seo: number;
|
|
pwa: number;
|
|
}
|
|
```
|
|
|
|
**Step 4: 创建可访问性测试类型**
|
|
|
|
```typescript
|
|
// test-framework/shared/types/accessibility.types.ts
|
|
export interface AccessibilityResult {
|
|
score: number;
|
|
violations: Violation[];
|
|
passes: number;
|
|
incomplete: number;
|
|
page: string;
|
|
url: string;
|
|
}
|
|
|
|
export interface Violation {
|
|
id: string;
|
|
impact: string;
|
|
description: string;
|
|
help: string;
|
|
helpUrl: string;
|
|
nodes: number;
|
|
}
|
|
|
|
export interface WCAGCompliance {
|
|
level: 'A' | 'AA' | 'AAA';
|
|
passed: number;
|
|
failed: number;
|
|
total: number;
|
|
}
|
|
```
|
|
|
|
**Step 5: 创建SEO测试类型**
|
|
|
|
```typescript
|
|
// test-framework/shared/types/seo.types.ts
|
|
export interface SEOResult {
|
|
score: number;
|
|
metaTags: MetaTagResult;
|
|
headings: HeadingResult;
|
|
links: LinkResult;
|
|
images: ImageResult;
|
|
}
|
|
|
|
export interface MetaTagResult {
|
|
title: boolean;
|
|
description: boolean;
|
|
keywords: boolean;
|
|
ogTitle: boolean;
|
|
ogDescription: boolean;
|
|
canonical: boolean;
|
|
}
|
|
|
|
export interface HeadingResult {
|
|
hasH1: boolean;
|
|
headingStructure: boolean;
|
|
multipleH1: boolean;
|
|
}
|
|
|
|
export interface LinkResult {
|
|
total: number;
|
|
broken: number;
|
|
internal: number;
|
|
external: number;
|
|
}
|
|
|
|
export interface ImageResult {
|
|
total: number;
|
|
withAlt: number;
|
|
withoutAlt: number;
|
|
}
|
|
```
|
|
|
|
**Step 6: 创建类型导出文件**
|
|
|
|
```typescript
|
|
// test-framework/shared/types/index.ts
|
|
export * from './page.types';
|
|
export * from './test.types';
|
|
export * from './performance.types';
|
|
export * from './accessibility.types';
|
|
export * from './seo.types';
|
|
```
|
|
|
|
**Step 7: 提交**
|
|
|
|
```bash
|
|
git add test-framework/shared/types/
|
|
git commit -m "feat: add type definitions for test framework"
|
|
```
|
|
|
|
### Task 3: 创建配置管理
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/config/base.config.ts`
|
|
- Create: `test-framework/shared/config/environments.ts`
|
|
- Create: `test-framework/shared/config/test-pages.ts`
|
|
- Create: `test-framework/shared/config/test-data.ts`
|
|
|
|
**Step 1: 创建基础配置**
|
|
|
|
```typescript
|
|
// test-framework/shared/config/base.config.ts
|
|
import { TestConfig } from '../types';
|
|
|
|
export const defaultConfig: TestConfig = {
|
|
baseURL: 'http://localhost:3000',
|
|
timeout: 5000,
|
|
retries: 3,
|
|
environment: 'development',
|
|
headless: true,
|
|
slowMo: undefined
|
|
};
|
|
|
|
export const testTimeouts = {
|
|
short: 2000,
|
|
medium: 5000,
|
|
long: 10000,
|
|
veryLong: 30000
|
|
};
|
|
|
|
export const testThresholds = {
|
|
performance: {
|
|
good: 90,
|
|
needsImprovement: 50,
|
|
poor: 0
|
|
},
|
|
accessibility: {
|
|
good: 95,
|
|
needsImprovement: 80,
|
|
poor: 0
|
|
},
|
|
seo: {
|
|
good: 90,
|
|
needsImprovement: 70,
|
|
poor: 0
|
|
}
|
|
};
|
|
```
|
|
|
|
**Step 2: 创建环境配置**
|
|
|
|
```typescript
|
|
// test-framework/shared/config/environments.ts
|
|
import { TestConfig } from '../types';
|
|
|
|
export const environments: Record<string, TestConfig> = {
|
|
development: {
|
|
baseURL: 'http://localhost:3000',
|
|
timeout: 5000,
|
|
retries: 3,
|
|
environment: 'development',
|
|
headless: false
|
|
},
|
|
staging: {
|
|
baseURL: 'https://staging.novalon.com',
|
|
timeout: 10000,
|
|
retries: 2,
|
|
environment: 'staging',
|
|
headless: true
|
|
},
|
|
production: {
|
|
baseURL: 'https://www.novalon.com',
|
|
timeout: 10000,
|
|
retries: 1,
|
|
environment: 'production',
|
|
headless: true
|
|
}
|
|
};
|
|
|
|
export function getEnvironmentConfig(env: string = 'development'): TestConfig {
|
|
return environments[env] || environments.development;
|
|
}
|
|
```
|
|
|
|
**Step 3: 创建测试页面配置**
|
|
|
|
```typescript
|
|
// test-framework/shared/config/test-pages.ts
|
|
import { PageConfig } from '../types';
|
|
|
|
export const testPages: Record<string, PageConfig> = {
|
|
home: {
|
|
name: '首页',
|
|
url: '/',
|
|
selectors: {
|
|
title: 'h1',
|
|
hero: '.hero-section',
|
|
features: '.features-section'
|
|
}
|
|
},
|
|
about: {
|
|
name: '关于我们',
|
|
url: '/about',
|
|
selectors: {
|
|
title: 'h1',
|
|
content: '.about-content'
|
|
}
|
|
},
|
|
contact: {
|
|
name: '联系我们',
|
|
url: '/contact',
|
|
selectors: {
|
|
title: 'h1',
|
|
form: '#contact-form',
|
|
submitButton: 'button[type="submit"]'
|
|
}
|
|
},
|
|
products: {
|
|
name: '产品',
|
|
url: '/products',
|
|
selectors: {
|
|
title: 'h1',
|
|
productGrid: '.products-grid',
|
|
productCard: '.product-card'
|
|
}
|
|
},
|
|
services: {
|
|
name: '服务',
|
|
url: '/services',
|
|
selectors: {
|
|
title: 'h1',
|
|
servicesList: '.services-list',
|
|
serviceItem: '.service-item'
|
|
}
|
|
},
|
|
cases: {
|
|
name: '案例',
|
|
url: '/cases',
|
|
selectors: {
|
|
title: 'h1',
|
|
casesGrid: '.cases-grid',
|
|
caseCard: '.case-card'
|
|
}
|
|
},
|
|
news: {
|
|
name: '新闻',
|
|
url: '/news',
|
|
selectors: {
|
|
title: 'h1',
|
|
newsList: '.news-list',
|
|
newsItem: '.news-item'
|
|
}
|
|
}
|
|
};
|
|
|
|
export function getPageConfig(pageKey: string): PageConfig {
|
|
return testPages[pageKey] || testPages.home;
|
|
}
|
|
|
|
export function getAllPageConfigs(): PageConfig[] {
|
|
return Object.values(testPages);
|
|
}
|
|
```
|
|
|
|
**Step 4: 创建测试数据配置**
|
|
|
|
```typescript
|
|
// test-framework/shared/config/test-data.ts
|
|
export const formData = {
|
|
valid: {
|
|
name: '测试用户',
|
|
email: 'test@example.com',
|
|
phone: '13800138000',
|
|
message: '这是一条测试消息'
|
|
},
|
|
invalid: {
|
|
email: 'invalid-email',
|
|
phone: '123',
|
|
empty: ''
|
|
}
|
|
};
|
|
|
|
export const performanceThresholds = {
|
|
loadTime: 3000,
|
|
domContentLoaded: 2000,
|
|
firstContentfulPaint: 1500,
|
|
largestContentfulPaint: 2500,
|
|
cumulativeLayoutShift: 0.1,
|
|
firstInputDelay: 100
|
|
};
|
|
|
|
export const accessibilityThresholds = {
|
|
score: 80,
|
|
maxViolations: 5
|
|
};
|
|
|
|
export const seoThresholds = {
|
|
score: 80,
|
|
minTitleLength: 10,
|
|
maxTitleLength: 60,
|
|
minDescriptionLength: 50,
|
|
maxDescriptionLength: 160
|
|
};
|
|
```
|
|
|
|
**Step 5: 提交**
|
|
|
|
```bash
|
|
git add test-framework/shared/config/
|
|
git commit -m "feat: add configuration management system"
|
|
```
|
|
|
|
### Task 4: 创建基础页面对象
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/pages/BasePage.ts`
|
|
|
|
**Step 1: 创建基础页面对象类**
|
|
|
|
```typescript
|
|
// test-framework/shared/pages/BasePage.ts
|
|
import { Page, Locator } from '@playwright/test';
|
|
import { TestConfig } from '../types';
|
|
import { defaultConfig } from '../config/base.config';
|
|
|
|
export class BasePage {
|
|
readonly page: Page;
|
|
readonly config: TestConfig;
|
|
readonly url: string;
|
|
|
|
constructor(page: Page, url: string, config?: TestConfig) {
|
|
this.page = page;
|
|
this.url = url;
|
|
this.config = config || defaultConfig;
|
|
}
|
|
|
|
async navigate(): Promise<void> {
|
|
await this.page.goto(this.url, { waitUntil: 'networkidle', timeout: this.config.timeout });
|
|
}
|
|
|
|
async waitForLoadState(state: 'load' | 'domcontentloaded' | 'networkidle' = 'load'): Promise<void> {
|
|
await this.page.waitForLoadState(state, { timeout: this.config.timeout });
|
|
}
|
|
|
|
async click(locator: Locator | string): Promise<void> {
|
|
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
|
await element.click({ timeout: this.config.timeout });
|
|
}
|
|
|
|
async fill(locator: Locator | string, value: string): Promise<void> {
|
|
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
|
await element.fill(value);
|
|
}
|
|
|
|
async getText(locator: Locator | string): Promise<string> {
|
|
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
|
return await element.textContent() || '';
|
|
}
|
|
|
|
async isVisible(locator: Locator | string): Promise<boolean> {
|
|
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
|
return await element.isVisible();
|
|
}
|
|
|
|
async waitForElement(locator: Locator | string, timeout?: number): Promise<void> {
|
|
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
|
await element.waitFor({ state: 'visible', timeout: timeout || this.config.timeout });
|
|
}
|
|
|
|
async scrollToElement(locator: Locator | string): Promise<void> {
|
|
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
|
await element.scrollIntoViewIfNeeded();
|
|
}
|
|
|
|
async takeScreenshot(filename: string): Promise<void> {
|
|
const screenshotDir = 'test-framework/reports/screenshots';
|
|
await this.page.screenshot({ path: `${screenshotDir}/${filename}` });
|
|
}
|
|
|
|
async hover(locator: Locator | string): Promise<void> {
|
|
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
|
await element.hover();
|
|
}
|
|
|
|
async getCurrentURL(): Promise<string> {
|
|
return this.page.url();
|
|
}
|
|
|
|
async getTitle(): Promise<string> {
|
|
return await this.page.title();
|
|
}
|
|
|
|
async getAttribute(locator: Locator | string, attribute: string): Promise<string | null> {
|
|
const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
|
|
return await element.getAttribute(attribute);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/shared/pages/BasePage.ts
|
|
git commit -m "feat: add base page object with common methods"
|
|
```
|
|
|
|
### Task 5: 创建具体页面对象
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/pages/HomePage.ts`
|
|
- Create: `test-framework/shared/pages/AboutPage.ts`
|
|
- Create: `test-framework/shared/pages/ContactPage.ts`
|
|
- Create: `test-framework/shared/pages/ProductsPage.ts`
|
|
- Create: `test-framework/shared/pages/ServicesPage.ts`
|
|
- Create: `test-framework/shared/pages/CasesPage.ts`
|
|
- Create: `test-framework/shared/pages/NewsPage.ts`
|
|
|
|
**Step 1: 创建首页页面对象**
|
|
|
|
```typescript
|
|
// test-framework/shared/pages/HomePage.ts
|
|
import { Page } from '@playwright/test';
|
|
import { BasePage } from './BasePage';
|
|
import { getPageConfig } from '../config/test-pages';
|
|
|
|
export class HomePage extends BasePage {
|
|
constructor(page: Page, config?) {
|
|
const pageConfig = getPageConfig('home');
|
|
super(page, pageConfig.url, config);
|
|
this.pageConfig = pageConfig;
|
|
}
|
|
|
|
private pageConfig;
|
|
|
|
async getHeroTitle(): Promise<string> {
|
|
return await this.getText('h1');
|
|
}
|
|
|
|
async getFeaturesSection(): Promise<boolean> {
|
|
return await this.isVisible('.features-section');
|
|
}
|
|
|
|
async navigateToAbout(): Promise<void> {
|
|
await this.click('a[href="/about"]');
|
|
}
|
|
|
|
async navigateToContact(): Promise<void> {
|
|
await this.click('a[href="/contact"]');
|
|
}
|
|
|
|
async navigateToProducts(): Promise<void> {
|
|
await this.click('a[href="/products"]');
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 创建关于页面页面对象**
|
|
|
|
```typescript
|
|
// test-framework/shared/pages/AboutPage.ts
|
|
import { Page } from '@playwright/test';
|
|
import { BasePage } from './BasePage';
|
|
import { getPageConfig } from '../config/test-pages';
|
|
|
|
export class AboutPage extends BasePage {
|
|
constructor(page: Page, config?) {
|
|
const pageConfig = getPageConfig('about');
|
|
super(page, pageConfig.url, config);
|
|
}
|
|
|
|
async getPageTitle(): Promise<string> {
|
|
return await this.getText('h1');
|
|
}
|
|
|
|
async getContent(): Promise<string> {
|
|
return await this.getText('.about-content');
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 3: 创建联系页面页面对象**
|
|
|
|
```typescript
|
|
// test-framework/shared/pages/ContactPage.ts
|
|
import { Page } from '@playwright/test';
|
|
import { BasePage } from './BasePage';
|
|
import { getPageConfig } from '../config/test-pages';
|
|
|
|
export class ContactPage extends BasePage {
|
|
constructor(page: Page, config?) {
|
|
const pageConfig = getPageConfig('contact');
|
|
super(page, pageConfig.url, config);
|
|
}
|
|
|
|
async fillContactForm(data: { name: string; email: string; phone: string; message: string }): Promise<void> {
|
|
await this.fill('#name', data.name);
|
|
await this.fill('#email', data.email);
|
|
await this.fill('#phone', data.phone);
|
|
await this.fill('#message', data.message);
|
|
}
|
|
|
|
async submitForm(): Promise<void> {
|
|
await this.click('button[type="submit"]');
|
|
}
|
|
|
|
async getFormErrorMessage(): Promise<string> {
|
|
return await this.getText('.error-message');
|
|
}
|
|
|
|
async getFormSuccessMessage(): Promise<string> {
|
|
return await this.getText('.success-message');
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 4: 创建产品页面页面对象**
|
|
|
|
```typescript
|
|
// test-framework/shared/pages/ProductsPage.ts
|
|
import { Page } from '@playwright/test';
|
|
import { BasePage } from './BasePage';
|
|
import { getPageConfig } from '../config/test-pages';
|
|
|
|
export class ProductsPage extends BasePage {
|
|
constructor(page: Page, config?) {
|
|
const pageConfig = getPageConfig('products');
|
|
super(page, pageConfig.url, config);
|
|
}
|
|
|
|
async getProductCount(): Promise<number> {
|
|
return await this.page.locator('.product-card').count();
|
|
}
|
|
|
|
async getProductTitle(index: number): Promise<string> {
|
|
return await this.getText(`.product-card:nth-child(${index + 1}) h3`);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 5: 创建服务页面页面对象**
|
|
|
|
```typescript
|
|
// test-framework/shared/pages/ServicesPage.ts
|
|
import { Page } from '@playwright/test';
|
|
import { BasePage } from './BasePage';
|
|
import { getPageConfig } from '../config/test-pages';
|
|
|
|
export class ServicesPage extends BasePage {
|
|
constructor(page: Page, config?) {
|
|
const pageConfig = getPageConfig('services');
|
|
super(page, pageConfig.url, config);
|
|
}
|
|
|
|
async getServiceCount(): Promise<number> {
|
|
return await this.page.locator('.service-item').count();
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 6: 创建案例页面页面对象**
|
|
|
|
```typescript
|
|
// test-framework/shared/pages/CasesPage.ts
|
|
import { Page } from '@playwright/test';
|
|
import { BasePage } from './BasePage';
|
|
import { getPageConfig } from '../config/test-pages';
|
|
|
|
export class CasesPage extends BasePage {
|
|
constructor(page: Page, config?) {
|
|
const pageConfig = getPageConfig('cases');
|
|
super(page, pageConfig.url, config);
|
|
}
|
|
|
|
async getCaseCount(): Promise<number> {
|
|
return await this.page.locator('.case-card').count();
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 7: 创建新闻页面页面对象**
|
|
|
|
```typescript
|
|
// test-framework/shared/pages/NewsPage.ts
|
|
import { Page } from '@playwright/test';
|
|
import { BasePage } from './BasePage';
|
|
import { getPageConfig } from '../config/test-pages';
|
|
|
|
export class NewsPage extends BasePage {
|
|
constructor(page: Page, config?) {
|
|
const pageConfig = getPageConfig('news');
|
|
super(page, pageConfig.url, config);
|
|
}
|
|
|
|
async getNewsCount(): Promise<number> {
|
|
return await this.page.locator('.news-item').count();
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 8: 创建页面对象导出文件**
|
|
|
|
```typescript
|
|
// test-framework/shared/pages/index.ts
|
|
export { BasePage } from './BasePage';
|
|
export { HomePage } from './HomePage';
|
|
export { AboutPage } from './AboutPage';
|
|
export { ContactPage } from './ContactPage';
|
|
export { ProductsPage } from './ProductsPage';
|
|
export { ServicesPage } from './ServicesPage';
|
|
export { CasesPage } from './CasesPage';
|
|
export { NewsPage } from './NewsPage';
|
|
```
|
|
|
|
**Step 9: 提交**
|
|
|
|
```bash
|
|
git add test-framework/shared/pages/
|
|
git commit -m "feat: add page objects for all pages"
|
|
```
|
|
|
|
### Task 6: 创建性能测试工具
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/utils/performance/PerformanceMonitor.ts`
|
|
- Create: `test-framework/shared/utils/performance/LighthouseRunner.ts`
|
|
- Create: `test-framework/shared/utils/performance/CoreWebVitals.ts`
|
|
|
|
**Step 1: 创建性能监控器**
|
|
|
|
```typescript
|
|
// test-framework/shared/utils/performance/PerformanceMonitor.ts
|
|
import { Page } from '@playwright/test';
|
|
import { PerformanceMetrics, NetworkTiming, ResourceTiming } from '../../types';
|
|
|
|
export class PerformanceMonitor {
|
|
constructor(private page: Page) {}
|
|
|
|
async measurePageLoad(): Promise<PerformanceMetrics> {
|
|
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 measureNetworkTiming(): Promise<NetworkTiming> {
|
|
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 measureResourceTiming(): Promise<ResourceTiming[]> {
|
|
return await this.page.evaluate(() => {
|
|
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
|
|
return resources.map(r => ({
|
|
name: r.name,
|
|
duration: r.duration,
|
|
size: r.transferSize,
|
|
type: r.initiatorType
|
|
}));
|
|
});
|
|
}
|
|
|
|
async measureMemoryUsage(): Promise<{ usedJSHeapSize: number; totalJSHeapSize: number }> {
|
|
return await this.page.evaluate(() => {
|
|
const memory = (performance as any).memory;
|
|
return {
|
|
usedJSHeapSize: memory.usedJSHeapSize,
|
|
totalJSHeapSize: memory.totalJSHeapSize
|
|
};
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/shared/utils/performance/PerformanceMonitor.ts
|
|
git commit -m "feat: add performance monitoring utility"
|
|
```
|
|
|
|
### Task 7: 创建可访问性测试工具
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/utils/accessibility/AccessibilityTester.ts`
|
|
|
|
**Step 1: 创建可访问性测试器**
|
|
|
|
```typescript
|
|
// test-framework/shared/utils/accessibility/AccessibilityTester.ts
|
|
import { Page } from '@playwright/test';
|
|
import AxeBuilder from '@axe-core/playwright';
|
|
import { AccessibilityResult, Violation } from '../../types';
|
|
|
|
export class AccessibilityTester {
|
|
constructor(private page: Page) {}
|
|
|
|
async runAxeScan(pageName: string, url: string): Promise<AccessibilityResult> {
|
|
const accessibilityScanResults = await new AxeBuilder({ this.page })
|
|
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
|
.analyze();
|
|
|
|
const violations: Violation[] = accessibilityScanResults.violations.map(v => ({
|
|
id: v.id,
|
|
impact: v.impact || 'unknown',
|
|
description: v.description,
|
|
help: v.help,
|
|
helpUrl: v.helpUrl,
|
|
nodes: v.nodes.length
|
|
}));
|
|
|
|
const passes = accessibilityScanResults.passes.length;
|
|
const incomplete = accessibilityScanResults.incomplete.length;
|
|
const score = this.calculateScore(violations, passes, incomplete);
|
|
|
|
return {
|
|
score,
|
|
violations,
|
|
passes,
|
|
incomplete,
|
|
page: pageName,
|
|
url
|
|
};
|
|
}
|
|
|
|
private calculateScore(violations: Violation[], passes: number, incomplete: number): number {
|
|
const total = violations.length + passes + incomplete;
|
|
if (total === 0) return 100;
|
|
return parseFloat(((passes / total) * 100).toFixed(1));
|
|
}
|
|
|
|
async checkColorContrast(): Promise<boolean> {
|
|
const results = await new AxeBuilder({ this.page })
|
|
.withTags(['wcag2aa'])
|
|
.include('#content')
|
|
.analyze();
|
|
|
|
return results.violations.filter(v => v.id === 'color-contrast').length === 0;
|
|
}
|
|
|
|
async checkAltText(): Promise<{ total: number; withAlt: number; withoutAlt: number }> {
|
|
const images = await this.page.locator('img').all();
|
|
let withAlt = 0;
|
|
let withoutAlt = 0;
|
|
|
|
for (const image of images) {
|
|
const alt = await image.getAttribute('alt');
|
|
if (alt && alt.trim() !== '') {
|
|
withAlt++;
|
|
} else {
|
|
withoutAlt++;
|
|
}
|
|
}
|
|
|
|
return {
|
|
total: images.length,
|
|
withAlt,
|
|
withoutAlt
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/shared/utils/accessibility/AccessibilityTester.ts
|
|
git commit -m "feat: add accessibility testing utility"
|
|
```
|
|
|
|
### Task 8: 创建SEO测试工具
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/utils/seo/SEOValidator.ts`
|
|
|
|
**Step 1: 创建SEO验证器**
|
|
|
|
```typescript
|
|
// test-framework/shared/utils/seo/SEOValidator.ts
|
|
import { Page } from '@playwright/test';
|
|
import { SEOResult, MetaTagResult, HeadingResult, LinkResult, ImageResult } from '../../types';
|
|
|
|
export class SEOValidator {
|
|
constructor(private page: Page) {}
|
|
|
|
async validateSEO(): Promise<SEOResult> {
|
|
const metaTags = await this.validateMetaTags();
|
|
const headings = await this.validateHeadings();
|
|
const links = await this.validateLinks();
|
|
const images = await this.validateImages();
|
|
|
|
const score = this.calculateScore(metaTags, headings, links, images);
|
|
|
|
return {
|
|
score,
|
|
metaTags,
|
|
headings,
|
|
links,
|
|
images
|
|
};
|
|
}
|
|
|
|
async validateMetaTags(): Promise<MetaTagResult> {
|
|
const title = await this.page.title();
|
|
const description = await this.page.getAttribute('meta[name="description"]', 'content');
|
|
const keywords = await this.page.getAttribute('meta[name="keywords"]', 'content');
|
|
const ogTitle = await this.page.getAttribute('meta[property="og:title"]', 'content');
|
|
const ogDescription = await this.page.getAttribute('meta[property="og:description"]', 'content');
|
|
const canonical = await this.page.getAttribute('link[rel="canonical"]', 'href');
|
|
|
|
return {
|
|
title: !!title && title.length >= 10 && title.length <= 60,
|
|
description: !!description && description.length >= 50 && description.length <= 160,
|
|
keywords: !!keywords,
|
|
ogTitle: !!ogTitle,
|
|
ogDescription: !!ogDescription,
|
|
canonical: !!canonical
|
|
};
|
|
}
|
|
|
|
async validateHeadings(): Promise<HeadingResult> {
|
|
const h1Count = await this.page.locator('h1').count();
|
|
const hasH1 = h1Count > 0;
|
|
const multipleH1 = h1Count > 1;
|
|
|
|
const headings = await this.page.evaluate(() => {
|
|
const elements = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
return Array.from(elements).map(el => el.tagName);
|
|
});
|
|
|
|
let headingStructure = true;
|
|
let previousLevel = 0;
|
|
for (const heading of headings) {
|
|
const level = parseInt(heading.charAt(1));
|
|
if (level > previousLevel + 1) {
|
|
headingStructure = false;
|
|
break;
|
|
}
|
|
previousLevel = level;
|
|
}
|
|
|
|
return {
|
|
hasH1,
|
|
headingStructure,
|
|
multipleH1
|
|
};
|
|
}
|
|
|
|
async validateLinks(): Promise<LinkResult> {
|
|
const links = await this.page.locator('a').all();
|
|
let internal = 0;
|
|
let external = 0;
|
|
let broken = 0;
|
|
|
|
for (const link of links) {
|
|
const href = await link.getAttribute('href');
|
|
if (!href) continue;
|
|
|
|
if (href.startsWith('http')) {
|
|
external++;
|
|
} else {
|
|
internal++;
|
|
}
|
|
}
|
|
|
|
return {
|
|
total: links.length,
|
|
broken,
|
|
internal,
|
|
external
|
|
};
|
|
}
|
|
|
|
async validateImages(): Promise<ImageResult> {
|
|
const images = await this.page.locator('img').all();
|
|
let withAlt = 0;
|
|
let withoutAlt = 0;
|
|
|
|
for (const image of images) {
|
|
const alt = await image.getAttribute('alt');
|
|
if (alt && alt.trim() !== '') {
|
|
withAlt++;
|
|
} else {
|
|
withoutAlt++;
|
|
}
|
|
}
|
|
|
|
return {
|
|
total: images.length,
|
|
withAlt,
|
|
withoutAlt
|
|
};
|
|
}
|
|
|
|
private calculateScore(metaTags: MetaTagResult, headings: HeadingResult, links: LinkResult, images: ImageResult): number {
|
|
let score = 0;
|
|
let total = 0;
|
|
|
|
const metaTagValues = Object.values(metaTags);
|
|
score += metaTagValues.filter(v => v).length;
|
|
total += metaTagValues.length;
|
|
|
|
if (headings.hasH1) score++;
|
|
if (headings.headingStructure) score++;
|
|
if (!headings.multipleH1) score++;
|
|
total += 3;
|
|
|
|
if (images.withoutAlt === 0) score++;
|
|
total++;
|
|
|
|
return Math.round((score / total) * 100);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/shared/utils/seo/SEOValidator.ts
|
|
git commit -m "feat: add SEO validation utility"
|
|
```
|
|
|
|
### Task 9: 创建共享Fixtures
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/fixtures/base.fixture.ts`
|
|
- Create: `test-framework/shared/fixtures/performance.fixture.ts`
|
|
- Create: `test-framework/shared/fixtures/accessibility.fixture.ts`
|
|
|
|
**Step 1: 创建基础fixture**
|
|
|
|
```typescript
|
|
// test-framework/shared/fixtures/base.fixture.ts
|
|
import { test as base } from '@playwright/test';
|
|
import { BasePage, HomePage, AboutPage, ContactPage } from '../pages';
|
|
import { getEnvironmentConfig } from '../config/environments';
|
|
|
|
type MyFixtures = {
|
|
basePage: BasePage;
|
|
homePage: HomePage;
|
|
aboutPage: AboutPage;
|
|
contactPage: ContactPage;
|
|
config: any;
|
|
};
|
|
|
|
export const test = base.extend<MyFixtures>({
|
|
config: async ({}, use) => {
|
|
const env = process.env.TEST_ENV || 'development';
|
|
const config = getEnvironmentConfig(env);
|
|
await use(config);
|
|
},
|
|
|
|
basePage: async ({ page }, use) => {
|
|
const basePage = new BasePage(page, '/');
|
|
await use(basePage);
|
|
},
|
|
|
|
homePage: async ({ page, config }, use) => {
|
|
const homePage = new HomePage(page, config);
|
|
await use(homePage);
|
|
},
|
|
|
|
aboutPage: async ({ page, config }, use) => {
|
|
const aboutPage = new AboutPage(page, config);
|
|
await use(aboutPage);
|
|
},
|
|
|
|
contactPage: async ({ page, config }, use) => {
|
|
const contactPage = new ContactPage(page, config);
|
|
await use(contactPage);
|
|
}
|
|
});
|
|
|
|
export { expect } from '@playwright/test';
|
|
```
|
|
|
|
**Step 2: 创建性能测试fixture**
|
|
|
|
```typescript
|
|
// test-framework/shared/fixtures/performance.fixture.ts
|
|
import { test as base } from '@playwright/test';
|
|
import { PerformanceMonitor } from '../utils/performance/PerformanceMonitor';
|
|
|
|
type MyFixtures = {
|
|
performanceMonitor: PerformanceMonitor;
|
|
};
|
|
|
|
export const test = base.extend<MyFixtures>({
|
|
performanceMonitor: async ({ page }, use) => {
|
|
const monitor = new PerformanceMonitor(page);
|
|
await use(monitor);
|
|
}
|
|
});
|
|
|
|
export { expect } from '@playwright/test';
|
|
```
|
|
|
|
**Step 3: 创建可访问性测试fixture**
|
|
|
|
```typescript
|
|
// test-framework/shared/fixtures/accessibility.fixture.ts
|
|
import { test as base } from '@playwright/test';
|
|
import { AccessibilityTester } from '../utils/accessibility/AccessibilityTester';
|
|
|
|
type MyFixtures = {
|
|
accessibilityTester: AccessibilityTester;
|
|
};
|
|
|
|
export const test = base.extend<MyFixtures>({
|
|
accessibilityTester: async ({ page }, use) => {
|
|
const tester = new AccessibilityTester(page);
|
|
await use(tester);
|
|
}
|
|
});
|
|
|
|
export { expect } from '@playwright/test';
|
|
```
|
|
|
|
**Step 4: 提交**
|
|
|
|
```bash
|
|
git add test-framework/shared/fixtures/
|
|
git commit -m "feat: add shared fixtures for testing"
|
|
```
|
|
|
|
### Task 10: 创建共享层导出文件
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/index.ts`
|
|
|
|
**Step 1: 创建共享层导出文件**
|
|
|
|
```typescript
|
|
// test-framework/shared/index.ts
|
|
export * from './config';
|
|
export * from './pages';
|
|
export * from './types';
|
|
export * from './fixtures';
|
|
export * from './utils/performance/PerformanceMonitor';
|
|
export * from './utils/accessibility/AccessibilityTester';
|
|
export * from './utils/seo/SEOValidator';
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/shared/index.ts
|
|
git commit -m "feat: add shared layer export file"
|
|
```
|
|
|
|
### Task 11: 验证共享层完整性
|
|
|
|
**Files:**
|
|
- Test: 验证所有导出是否正常
|
|
|
|
**Step 1: 创建验证脚本**
|
|
|
|
```typescript
|
|
// test-framework/verify-shared-layer.ts
|
|
import { BasePage, HomePage } from './shared/pages';
|
|
import { getEnvironmentConfig } from './shared/config/environments';
|
|
import { PerformanceMonitor } from './shared/utils/performance/PerformanceMonitor';
|
|
import { AccessibilityTester } from './shared/utils/accessibility/AccessibilityTester';
|
|
import { SEOValidator } from './shared/utils/seo/SEOValidator';
|
|
|
|
console.log('✅ Shared layer imports verified successfully!');
|
|
console.log('- BasePage: OK');
|
|
console.log('- HomePage: OK');
|
|
console.log('- getEnvironmentConfig: OK');
|
|
console.log('- PerformanceMonitor: OK');
|
|
console.log('- AccessibilityTester: OK');
|
|
console.log('- SEOValidator: OK');
|
|
```
|
|
|
|
**Step 2: 运行验证脚本**
|
|
|
|
Run: `npx ts-node test-framework/verify-shared-layer.ts`
|
|
|
|
Expected: 所有导入成功,无错误
|
|
|
|
**Step 3: 提交**
|
|
|
|
```bash
|
|
git add test-framework/verify-shared-layer.ts
|
|
git commit -m "test: add shared layer verification script"
|
|
```
|
|
|
|
### Task 12: 更新E2E测试以使用共享层
|
|
|
|
**Files:**
|
|
- Modify: `e2e/src/pages/BasePage.ts`
|
|
- Modify: `e2e/src/pages/HomePage.ts`
|
|
- Modify: `e2e/src/pages/AboutPage.ts`
|
|
- Modify: `e2e/src/pages/ContactPage.ts`
|
|
|
|
**Step 1: 更新E2E测试的BasePage**
|
|
|
|
```typescript
|
|
// e2e/src/pages/BasePage.ts
|
|
import { Page, Locator } from '@playwright/test';
|
|
import { BasePage as SharedBasePage } from '../../test-framework/shared/pages';
|
|
|
|
export class BasePage extends SharedBasePage {
|
|
constructor(page: Page, url: string, config?) {
|
|
super(page, url, config);
|
|
}
|
|
|
|
// E2E测试特有的方法可以在这里添加
|
|
}
|
|
```
|
|
|
|
**Step 2: 更新E2E测试的HomePage**
|
|
|
|
```typescript
|
|
// e2e/src/pages/HomePage.ts
|
|
import { Page } from '@playwright/test';
|
|
import { HomePage as SharedHomePage } from '../../test-framework/shared/pages';
|
|
|
|
export class HomePage extends SharedHomePage {
|
|
constructor(page: Page, config?) {
|
|
super(page, config);
|
|
}
|
|
|
|
// E2E测试特有的方法可以在这里添加
|
|
}
|
|
```
|
|
|
|
**Step 3: 运行E2E测试验证**
|
|
|
|
Run: `cd e2e && npm run test:smoke`
|
|
|
|
Expected: 所有测试通过
|
|
|
|
**Step 4: 提交**
|
|
|
|
```bash
|
|
git add e2e/src/pages/
|
|
git commit -m "refactor: update E2E pages to use shared layer"
|
|
```
|
|
|
|
### Task 13: 创建阶段1总结文档
|
|
|
|
**Files:**
|
|
- Create: `test-framework/docs/phase1-summary.md`
|
|
|
|
**Step 1: 创建阶段1总结文档**
|
|
|
|
```markdown
|
|
# 阶段1:创建共享层 - 完成总结
|
|
|
|
## 完成的任务
|
|
|
|
1. ✅ 创建共享层目录结构
|
|
2. ✅ 创建类型定义
|
|
3. ✅ 创建配置管理
|
|
4. ✅ 创建基础页面对象
|
|
5. ✅ 创建具体页面对象
|
|
6. ✅ 创建性能测试工具
|
|
7. ✅ 创建可访问性测试工具
|
|
8. ✅ 创建SEO测试工具
|
|
9. ✅ 创建共享Fixtures
|
|
10. ✅ 创建共享层导出文件
|
|
11. ✅ 验证共享层完整性
|
|
12. ✅ 更新E2E测试以使用共享层
|
|
|
|
## 创建的文件
|
|
|
|
### 配置文件
|
|
- test-framework/shared/config/base.config.ts
|
|
- test-framework/shared/config/environments.ts
|
|
- test-framework/shared/config/test-pages.ts
|
|
- test-framework/shared/config/test-data.ts
|
|
|
|
### 页面对象
|
|
- test-framework/shared/pages/BasePage.ts
|
|
- test-framework/shared/pages/HomePage.ts
|
|
- test-framework/shared/pages/AboutPage.ts
|
|
- test-framework/shared/pages/ContactPage.ts
|
|
- test-framework/shared/pages/ProductsPage.ts
|
|
- test-framework/shared/pages/ServicesPage.ts
|
|
- test-framework/shared/pages/CasesPage.ts
|
|
- test-framework/shared/pages/NewsPage.ts
|
|
|
|
### 工具类
|
|
- test-framework/shared/utils/performance/PerformanceMonitor.ts
|
|
- test-framework/shared/utils/accessibility/AccessibilityTester.ts
|
|
- test-framework/shared/utils/seo/SEOValidator.ts
|
|
|
|
### Fixtures
|
|
- test-framework/shared/fixtures/base.fixture.ts
|
|
- test-framework/shared/fixtures/performance.fixture.ts
|
|
- test-framework/shared/fixtures/accessibility.fixture.ts
|
|
|
|
### 类型定义
|
|
- test-framework/shared/types/page.types.ts
|
|
- test-framework/shared/types/test.types.ts
|
|
- test-framework/shared/types/performance.types.ts
|
|
- test-framework/shared/types/accessibility.types.ts
|
|
- test-framework/shared/types/seo.types.ts
|
|
|
|
## 验证结果
|
|
|
|
- ✅ 所有TypeScript编译无错误
|
|
- ✅ 所有导入正常工作
|
|
- ✅ E2E测试仍然正常运行
|
|
- ✅ 共享层结构完整
|
|
|
|
## 下一步
|
|
|
|
进入阶段2:迁移开发环境测试
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/docs/phase1-summary.md
|
|
git commit -m "docs: add phase 1 completion summary"
|
|
```
|
|
|
|
---
|
|
|
|
## 阶段2:迁移开发环境测试
|
|
|
|
### Task 14: 创建开发环境测试基础结构
|
|
|
|
**Files:**
|
|
- Create: `test-framework/dev-audit/performance/performance.spec.ts`
|
|
- Create: `test-framework/dev-audit/seo/seo.spec.ts`
|
|
- Create: `test-framework/dev-audit/accessibility/accessibility.spec.ts`
|
|
- Create: `test-framework/dev-audit/forms/forms.spec.ts`
|
|
|
|
**Step 1: 创建性能审计测试**
|
|
|
|
```typescript
|
|
// test-framework/dev-audit/performance/performance.spec.ts
|
|
import { test, expect } from '@playwright/test';
|
|
import { HomePage, AboutPage, ContactPage, ProductsPage, ServicesPage, CasesPage, NewsPage } from '../../shared/pages';
|
|
import { PerformanceMonitor } from '../../shared/utils/performance/PerformanceMonitor';
|
|
import { performanceThresholds } from '../../shared/config/test-data';
|
|
|
|
test.describe('性能审计测试', () => {
|
|
const pages = [
|
|
{ name: '首页', PageClass: HomePage },
|
|
{ name: '关于我们', PageClass: AboutPage },
|
|
{ name: '联系我们', PageClass: ContactPage },
|
|
{ name: '产品', PageClass: ProductsPage },
|
|
{ name: '服务', PageClass: ServicesPage },
|
|
{ name: '案例', PageClass: CasesPage },
|
|
{ name: '新闻', PageClass: NewsPage }
|
|
];
|
|
|
|
pages.forEach(({ name, PageClass }) => {
|
|
test(`${name} - 页面加载性能`, async ({ page }) => {
|
|
const pageObj = new PageClass(page);
|
|
const monitor = new PerformanceMonitor(page);
|
|
|
|
await pageObj.navigate();
|
|
const metrics = await monitor.measurePageLoad();
|
|
|
|
console.log(`${name} 性能指标:`, metrics);
|
|
|
|
expect(metrics.loadTime).toBeLessThan(performanceThresholds.loadTime);
|
|
expect(metrics.domContentLoaded).toBeLessThan(performanceThresholds.domContentLoaded);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
**Step 2: 创建SEO检查测试**
|
|
|
|
```typescript
|
|
// test-framework/dev-audit/seo/seo.spec.ts
|
|
import { test, expect } from '@playwright/test';
|
|
import { HomePage, AboutPage, ContactPage } from '../../shared/pages';
|
|
import { SEOValidator } from '../../shared/utils/seo/SEOValidator';
|
|
import { seoThresholds } from '../../shared/config/test-data';
|
|
|
|
test.describe('SEO检查测试', () => {
|
|
const pages = [
|
|
{ name: '首页', PageClass: HomePage },
|
|
{ name: '关于我们', PageClass: AboutPage },
|
|
{ name: '联系我们', PageClass: ContactPage }
|
|
];
|
|
|
|
pages.forEach(({ name, PageClass }) => {
|
|
test(`${name} - SEO验证`, async ({ page }) => {
|
|
const pageObj = new PageClass(page);
|
|
const validator = new SEOValidator(page);
|
|
|
|
await pageObj.navigate();
|
|
const result = await validator.validateSEO();
|
|
|
|
console.log(`${name} SEO结果:`, result);
|
|
|
|
expect(result.score).toBeGreaterThanOrEqual(seoThresholds.score);
|
|
expect(result.metaTags.title).toBe(true);
|
|
expect(result.metaTags.description).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
**Step 3: 创建可访问性测试**
|
|
|
|
```typescript
|
|
// test-framework/dev-audit/accessibility/accessibility.spec.ts
|
|
import { test, expect } from '@playwright/test';
|
|
import { HomePage, AboutPage, ContactPage } from '../../shared/pages';
|
|
import { AccessibilityTester } from '../../shared/utils/accessibility/AccessibilityTester';
|
|
import { accessibilityThresholds } from '../../shared/config/test-data';
|
|
import { getPageConfig } from '../../shared/config/test-pages';
|
|
|
|
test.describe('可访问性测试', () => {
|
|
const pages = [
|
|
{ name: '首页', PageClass: HomePage },
|
|
{ name: '关于我们', PageClass: AboutPage },
|
|
{ name: '联系我们', PageClass: ContactPage }
|
|
];
|
|
|
|
pages.forEach(({ name, PageClass }) => {
|
|
test(`${name} - 可访问性验证`, async ({ page }) => {
|
|
const pageConfig = getPageConfig(name === '首页' ? 'home' : name === '关于我们' ? 'about' : 'contact');
|
|
const pageObj = new PageClass(page);
|
|
const tester = new AccessibilityTester(page);
|
|
|
|
await pageObj.navigate();
|
|
const result = await tester.runAxeScan(pageConfig.name, pageConfig.url);
|
|
|
|
console.log(`${name} 可访问性结果:`, result);
|
|
|
|
expect(result.score).toBeGreaterThanOrEqual(accessibilityThresholds.score);
|
|
expect(result.violations.length).toBeLessThanOrEqual(accessibilityThresholds.maxViolations);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
**Step 4: 创建表单验证测试**
|
|
|
|
```typescript
|
|
// test-framework/dev-audit/forms/forms.spec.ts
|
|
import { test, expect } from '@playwright/test';
|
|
import { ContactPage } from '../../shared/pages';
|
|
import { formData } from '../../shared/config/test-data';
|
|
|
|
test.describe('表单验证测试', () => {
|
|
test('联系表单 - 有效数据提交', async ({ page }) => {
|
|
const contactPage = new ContactPage(page);
|
|
|
|
await contactPage.navigate();
|
|
await contactPage.fillContactForm(formData.valid);
|
|
await contactPage.submitForm();
|
|
|
|
const successMessage = await contactPage.getFormSuccessMessage();
|
|
expect(successMessage).toContain('成功');
|
|
});
|
|
|
|
test('联系表单 - 必填字段验证', async ({ page }) => {
|
|
const contactPage = new ContactPage(page);
|
|
|
|
await contactPage.navigate();
|
|
await contactPage.fillContactForm({
|
|
name: '',
|
|
email: '',
|
|
phone: '',
|
|
message: ''
|
|
});
|
|
await contactPage.submitForm();
|
|
|
|
const errorMessage = await contactPage.getFormErrorMessage();
|
|
expect(errorMessage).toBeTruthy();
|
|
});
|
|
|
|
test('联系表单 - 邮箱格式验证', async ({ page }) => {
|
|
const contactPage = new ContactPage(page);
|
|
|
|
await contactPage.navigate();
|
|
await contactPage.fillContactForm({
|
|
name: '测试用户',
|
|
email: formData.invalid.email,
|
|
phone: '13800138000',
|
|
message: '测试消息'
|
|
});
|
|
await contactPage.submitForm();
|
|
|
|
const errorMessage = await contactPage.getFormErrorMessage();
|
|
expect(errorMessage).toContain('邮箱');
|
|
});
|
|
});
|
|
```
|
|
|
|
**Step 5: 提交**
|
|
|
|
```bash
|
|
git add test-framework/dev-audit/
|
|
git commit -m "feat: add dev-audit test suites"
|
|
```
|
|
|
|
### Task 15: 创建Playwright配置文件
|
|
|
|
**Files:**
|
|
- Create: `test-framework/playwright.config.ts`
|
|
|
|
**Step 1: 创建Playwright配置**
|
|
|
|
```typescript
|
|
// test-framework/playwright.config.ts
|
|
import { defineConfig, devices } from '@playwright/test';
|
|
import { getEnvironmentConfig } from './shared/config/environments';
|
|
|
|
const config = defineConfig({
|
|
testDir: './dev-audit',
|
|
fullyParallel: true,
|
|
forbidOnly: !!process.env.CI,
|
|
retries: process.env.CI ? 2 : 0,
|
|
workers: process.env.CI ? 1 : undefined,
|
|
reporter: [
|
|
['html', { outputFolder: 'test-framework/reports/html' }],
|
|
['json', { outputFile: 'test-framework/reports/results.json' }],
|
|
['list']
|
|
],
|
|
use: {
|
|
baseURL: getEnvironmentConfig(process.env.TEST_ENV || 'development').baseURL,
|
|
trace: 'on-first-retry',
|
|
screenshot: 'only-on-failure',
|
|
video: 'retain-on-failure'
|
|
},
|
|
projects: [
|
|
{
|
|
name: 'chromium',
|
|
use: { ...devices['Desktop Chrome'] },
|
|
},
|
|
{
|
|
name: 'firefox',
|
|
use: { ...devices['Desktop Firefox'] },
|
|
},
|
|
{
|
|
name: 'webkit',
|
|
use: { ...devices['Desktop Safari'] },
|
|
},
|
|
{
|
|
name: 'Mobile Chrome',
|
|
use: { ...devices['Pixel 5'] },
|
|
},
|
|
{
|
|
name: 'Mobile Safari',
|
|
use: { ...devices['iPhone 12'] },
|
|
}
|
|
]
|
|
});
|
|
|
|
export default config;
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/playwright.config.ts
|
|
git commit -m "feat: add Playwright configuration for dev-audit"
|
|
```
|
|
|
|
### Task 16: 创建package.json和脚本
|
|
|
|
**Files:**
|
|
- Create: `test-framework/package.json`
|
|
|
|
**Step 1: 创建package.json**
|
|
|
|
```json
|
|
{
|
|
"name": "test-framework",
|
|
"version": "1.0.0",
|
|
"description": "Unified test framework for Novalon website",
|
|
"scripts": {
|
|
"test": "playwright test",
|
|
"test:dev-audit": "playwright test",
|
|
"test:dev-audit:performance": "playwright test dev-audit/performance",
|
|
"test:dev-audit:seo": "playwright test dev-audit/seo",
|
|
"test:dev-audit:accessibility": "playwright test dev-audit/accessibility",
|
|
"test:dev-audit:forms": "playwright test dev-audit/forms",
|
|
"test:report": "playwright show-report",
|
|
"test:install": "playwright install"
|
|
},
|
|
"devDependencies": {
|
|
"@playwright/test": "^1.40.0",
|
|
"@axe-core/playwright": "^4.8.0",
|
|
"typescript": "^5.3.0"
|
|
},
|
|
"dependencies": {
|
|
"lighthouse": "^11.0.0",
|
|
"chrome-launcher": "^1.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/package.json
|
|
git commit -m "feat: add package.json with test scripts"
|
|
```
|
|
|
|
### Task 17: 创建TypeScript配置
|
|
|
|
**Files:**
|
|
- Create: `test-framework/tsconfig.json`
|
|
|
|
**Step 1: 创建TypeScript配置**
|
|
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"target": "ES2020",
|
|
"module": "commonjs",
|
|
"lib": ["ES2020"],
|
|
"outDir": "./dist",
|
|
"rootDir": "./",
|
|
"strict": true,
|
|
"esModuleInterop": true,
|
|
"skipLibCheck": true,
|
|
"forceConsistentCasingInFileNames": true,
|
|
"resolveJsonModule": true,
|
|
"moduleResolution": "node",
|
|
"types": ["node", "@playwright/test"]
|
|
},
|
|
"include": [
|
|
"shared/**/*",
|
|
"dev-audit/**/*"
|
|
],
|
|
"exclude": [
|
|
"node_modules",
|
|
"dist",
|
|
"reports"
|
|
]
|
|
}
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/tsconfig.json
|
|
git commit -m "feat: add TypeScript configuration"
|
|
```
|
|
|
|
### Task 18: 安装依赖并验证
|
|
|
|
**Files:**
|
|
- Test: 验证依赖安装和测试运行
|
|
|
|
**Step 1: 安装依赖**
|
|
|
|
Run: `cd test-framework && npm install`
|
|
|
|
Expected: 所有依赖安装成功
|
|
|
|
**Step 2: 安装Playwright浏览器**
|
|
|
|
Run: `cd test-framework && npm run test:install`
|
|
|
|
Expected: Playwright浏览器安装成功
|
|
|
|
**Step 3: 运行测试验证**
|
|
|
|
Run: `cd test-framework && npm run test:dev-audit:performance`
|
|
|
|
Expected: 性能测试运行成功
|
|
|
|
**Step 4: 提交**
|
|
|
|
```bash
|
|
git add test-framework/package-lock.json
|
|
git commit -m "chore: install dependencies and verify tests"
|
|
```
|
|
|
|
### Task 19: 创建阶段2总结文档
|
|
|
|
**Files:**
|
|
- Create: `test-framework/docs/phase2-summary.md`
|
|
|
|
**Step 1: 创建阶段2总结文档**
|
|
|
|
```markdown
|
|
# 阶段2:迁移开发环境测试 - 完成总结
|
|
|
|
## 完成的任务
|
|
|
|
1. ✅ 创建开发环境测试基础结构
|
|
2. ✅ 创建性能审计测试
|
|
3. ✅ 创建SEO检查测试
|
|
4. ✅ 创建可访问性测试
|
|
5. ✅ 创建表单验证测试
|
|
6. ✅ 创建Playwright配置文件
|
|
7. ✅ 创建package.json和脚本
|
|
8. ✅ 创建TypeScript配置
|
|
9. ✅ 安装依赖并验证
|
|
|
|
## 创建的文件
|
|
|
|
### 测试文件
|
|
- test-framework/dev-audit/performance/performance.spec.ts
|
|
- test-framework/dev-audit/seo/seo.spec.ts
|
|
- test-framework/dev-audit/accessibility/accessibility.spec.ts
|
|
- test-framework/dev-audit/forms/forms.spec.ts
|
|
|
|
### 配置文件
|
|
- test-framework/playwright.config.ts
|
|
- test-framework/package.json
|
|
- test-framework/tsconfig.json
|
|
|
|
## 验证结果
|
|
|
|
- ✅ 所有TypeScript编译无错误
|
|
- ✅ 所有依赖安装成功
|
|
- ✅ Playwright浏览器安装成功
|
|
- ✅ 测试可以正常运行
|
|
- ✅ 测试结果与原有脚本一致
|
|
|
|
## 下一步
|
|
|
|
进入阶段3:优化和清理
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/docs/phase2-summary.md
|
|
git commit -m "docs: add phase 2 completion summary"
|
|
```
|
|
|
|
---
|
|
|
|
## 阶段3:优化和清理
|
|
|
|
### Task 20: 移除旧的scripts目录
|
|
|
|
**Files:**
|
|
- Delete: `scripts/` directory
|
|
|
|
**Step 1: 备份scripts目录**
|
|
|
|
Run: `cp -r scripts scripts.backup`
|
|
|
|
Expected: 备份创建成功
|
|
|
|
**Step 2: 删除scripts目录**
|
|
|
|
Run: `rm -rf scripts/`
|
|
|
|
Expected: scripts目录删除成功
|
|
|
|
**Step 3: 提交**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "chore: remove old scripts directory (backed up to scripts.backup)"
|
|
```
|
|
|
|
### Task 21: 创建综合测试报告生成器
|
|
|
|
**Files:**
|
|
- Create: `test-framework/shared/utils/reporting/TestReporter.ts`
|
|
|
|
**Step 1: 创建测试报告生成器**
|
|
|
|
```typescript
|
|
// test-framework/shared/utils/reporting/TestReporter.ts
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { TestSuite, AccessibilityResult, SEOResult, PerformanceMetrics } from '../../types';
|
|
|
|
export class TestReporter {
|
|
private results: Map<string, any> = new Map();
|
|
|
|
addResult(type: string, result: any): void {
|
|
this.results.set(type, result);
|
|
}
|
|
|
|
generateHTMLReport(): string {
|
|
const timestamp = new Date().toLocaleString('zh-CN');
|
|
|
|
let html = `
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>综合测试报告 - ${timestamp}</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: #f5f5f5;
|
|
}
|
|
.header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 10px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.section {
|
|
background: white;
|
|
padding: 25px;
|
|
border-radius: 10px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
.metric {
|
|
display: inline-block;
|
|
padding: 10px 20px;
|
|
margin: 5px;
|
|
border-radius: 5px;
|
|
font-weight: bold;
|
|
}
|
|
.metric.success { background: #10b981; color: white; }
|
|
.metric.warning { background: #f59e0b; color: white; }
|
|
.metric.danger { background: #ef4444; color: white; }
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 15px;
|
|
}
|
|
th, td {
|
|
padding: 12px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
th { background: #f9fafb; font-weight: bold; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>综合测试报告</h1>
|
|
<p>生成时间: ${timestamp}</p>
|
|
</div>
|
|
`;
|
|
|
|
for (const [type, result] of this.results.entries()) {
|
|
html += this.generateSection(type, result);
|
|
}
|
|
|
|
html += `
|
|
</body>
|
|
</html>`;
|
|
|
|
return html;
|
|
}
|
|
|
|
private generateSection(type: string, result: any): string {
|
|
switch (type) {
|
|
case 'accessibility':
|
|
return this.generateAccessibilitySection(result);
|
|
case 'seo':
|
|
return this.generateSEOSection(result);
|
|
case 'performance':
|
|
return this.generatePerformanceSection(result);
|
|
default:
|
|
return `<div class="section"><h2>${type}</h2><pre>${JSON.stringify(result, null, 2)}</pre></div>`;
|
|
}
|
|
}
|
|
|
|
private generateAccessibilitySection(results: AccessibilityResult[]): string {
|
|
const totalViolations = results.reduce((sum, r) => sum + r.violations.length, 0);
|
|
const avgScore = results.reduce((sum, r) => sum + r.score, 0) / results.length;
|
|
|
|
return `
|
|
<div class="section">
|
|
<h2>可访问性测试</h2>
|
|
<div>
|
|
<span class="metric ${avgScore >= 80 ? 'success' : 'warning'}">平均分数: ${avgScore.toFixed(1)}</span>
|
|
<span class="metric ${totalViolations <= 5 ? 'success' : 'danger'}">总违规数: ${totalViolations}</span>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>页面</th>
|
|
<th>分数</th>
|
|
<th>违规数</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${results.map(r => `
|
|
<tr>
|
|
<td>${r.page}</td>
|
|
<td>${r.score}</td>
|
|
<td>${r.violations.length}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
}
|
|
|
|
private generateSEOSection(results: SEOResult[]): string {
|
|
const avgScore = results.reduce((sum, r) => sum + r.score, 0) / results.length;
|
|
|
|
return `
|
|
<div class="section">
|
|
<h2>SEO检查</h2>
|
|
<div>
|
|
<span class="metric ${avgScore >= 80 ? 'success' : 'warning'}">平均分数: ${avgScore.toFixed(1)}</span>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>页面</th>
|
|
<th>分数</th>
|
|
<th>Meta标签</th>
|
|
<th>标题</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${results.map(r => `
|
|
<tr>
|
|
<td>${r.page}</td>
|
|
<td>${r.score}</td>
|
|
<td>${r.metaTags.title && r.metaTags.description ? '✅' : '❌'}</td>
|
|
<td>${r.headings.hasH1 ? '✅' : '❌'}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
}
|
|
|
|
private generatePerformanceSection(results: PerformanceMetrics[]): string {
|
|
const avgLoadTime = results.reduce((sum, r) => sum + r.loadTime, 0) / results.length;
|
|
|
|
return `
|
|
<div class="section">
|
|
<h2>性能测试</h2>
|
|
<div>
|
|
<span class="metric ${avgLoadTime <= 3000 ? 'success' : 'warning'}">平均加载时间: ${avgLoadTime.toFixed(0)}ms</span>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>页面</th>
|
|
<th>加载时间</th>
|
|
<th>DOM加载</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${results.map(r => `
|
|
<tr>
|
|
<td>${r.page}</td>
|
|
<td>${r.loadTime}ms</td>
|
|
<td>${r.domContentLoaded}ms</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
}
|
|
|
|
saveHTMLReport(outputPath: string): void {
|
|
const html = this.generateHTMLReport();
|
|
const dir = path.dirname(outputPath);
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(outputPath, html, 'utf-8');
|
|
}
|
|
|
|
generateJSONReport(): any {
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
results: Object.fromEntries(this.results)
|
|
};
|
|
}
|
|
|
|
saveJSONReport(outputPath: string): void {
|
|
const json = this.generateJSONReport();
|
|
const dir = path.dirname(outputPath);
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(outputPath, JSON.stringify(json, null, 2), 'utf-8');
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/shared/utils/reporting/TestReporter.ts
|
|
git commit -m "feat: add comprehensive test report generator"
|
|
```
|
|
|
|
### Task 22: 创建一键测试脚本
|
|
|
|
**Files:**
|
|
- Create: `test-framework/run-all-tests.sh`
|
|
|
|
**Step 1: 创建一键测试脚本**
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
|
|
echo "🚀 开始运行所有测试..."
|
|
|
|
echo "📊 运行性能审计..."
|
|
npm run test:dev-audit:performance
|
|
|
|
echo "🔍 运行SEO检查..."
|
|
npm run test:dev-audit:seo
|
|
|
|
echo "♿ 运行可访问性测试..."
|
|
npm run test:dev-audit:accessibility
|
|
|
|
echo "📝 运行表单验证..."
|
|
npm run test:dev-audit:forms
|
|
|
|
echo "📈 生成综合报告..."
|
|
npm run test:report
|
|
|
|
echo "✅ 所有测试完成!"
|
|
echo "📄 报告位置: test-framework/reports/html/index.html"
|
|
```
|
|
|
|
**Step 2: 设置脚本权限**
|
|
|
|
Run: `chmod +x test-framework/run-all-tests.sh`
|
|
|
|
Expected: 脚本权限设置成功
|
|
|
|
**Step 3: 提交**
|
|
|
|
```bash
|
|
git add test-framework/run-all-tests.sh
|
|
git commit -m "feat: add one-click test runner script"
|
|
```
|
|
|
|
### Task 23: 创建README文档
|
|
|
|
**Files:**
|
|
- Create: `test-framework/README.md`
|
|
|
|
**Step 1: 创建README文档**
|
|
|
|
```markdown
|
|
# 测试框架
|
|
|
|
统一的测试框架,整合了E2E测试和开发环境测试。
|
|
|
|
## 目录结构
|
|
|
|
```
|
|
test-framework/
|
|
├── shared/ # 共享层
|
|
│ ├── config/ # 配置管理
|
|
│ ├── pages/ # 页面对象
|
|
│ ├── utils/ # 工具类
|
|
│ ├── fixtures/ # 测试fixtures
|
|
│ └── types/ # 类型定义
|
|
├── dev-audit/ # 开发环境测试
|
|
│ ├── performance/ # 性能测试
|
|
│ ├── seo/ # SEO测试
|
|
│ ├── accessibility/ # 可访问性测试
|
|
│ └── forms/ # 表单测试
|
|
└── reports/ # 测试报告
|
|
```
|
|
|
|
## 快速开始
|
|
|
|
### 安装依赖
|
|
|
|
```bash
|
|
npm install
|
|
npm run test:install
|
|
```
|
|
|
|
### 运行测试
|
|
|
|
```bash
|
|
# 运行所有测试
|
|
npm run test
|
|
|
|
# 运行特定类型的测试
|
|
npm run test:dev-audit:performance
|
|
npm run test:dev-audit:seo
|
|
npm run test:dev-audit:accessibility
|
|
npm run test:dev-audit:forms
|
|
|
|
# 一键运行所有测试
|
|
./run-all-tests.sh
|
|
```
|
|
|
|
### 查看报告
|
|
|
|
```bash
|
|
npm run test:report
|
|
```
|
|
|
|
## 测试类型
|
|
|
|
### 性能测试
|
|
- 页面加载性能
|
|
- Core Web Vitals
|
|
- 资源加载时间
|
|
|
|
### SEO测试
|
|
- Meta标签验证
|
|
- 标题结构检查
|
|
- 链接验证
|
|
|
|
### 可访问性测试
|
|
- WCAG 2.1 AA合规性
|
|
- 颜色对比度检查
|
|
- Alt文本验证
|
|
|
|
### 表单测试
|
|
- 必填字段验证
|
|
- 格式验证
|
|
- 提交功能测试
|
|
|
|
## 配置
|
|
|
|
测试配置位于 `shared/config/` 目录:
|
|
|
|
- `base.config.ts` - 基础配置
|
|
- `environments.ts` - 环境配置
|
|
- `test-pages.ts` - 测试页面配置
|
|
- `test-data.ts` - 测试数据配置
|
|
|
|
## 页面对象
|
|
|
|
所有页面对象位于 `shared/pages/` 目录,继承自 `BasePage`。
|
|
|
|
## 工具类
|
|
|
|
- `PerformanceMonitor` - 性能监控
|
|
- `AccessibilityTester` - 可访问性测试
|
|
- `SEOValidator` - SEO验证
|
|
|
|
## 贡献
|
|
|
|
请遵循项目的代码规范和测试最佳实践。
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/README.md
|
|
git commit -m "docs: add comprehensive README documentation"
|
|
```
|
|
|
|
### Task 24: 最终验证和测试
|
|
|
|
**Files:**
|
|
- Test: 验证所有功能正常工作
|
|
|
|
**Step 1: 运行完整测试套件**
|
|
|
|
Run: `cd test-framework && ./run-all-tests.sh`
|
|
|
|
Expected: 所有测试通过
|
|
|
|
**Step 2: 验证报告生成**
|
|
|
|
Run: `ls -la test-framework/reports/html/`
|
|
|
|
Expected: HTML报告生成成功
|
|
|
|
**Step 3: 验证JSON报告**
|
|
|
|
Run: `ls -la test-framework/reports/json/`
|
|
|
|
Expected: JSON报告生成成功
|
|
|
|
**Step 4: 提交**
|
|
|
|
```bash
|
|
git add test-framework/reports/
|
|
git commit -m "test: verify all tests and reports"
|
|
```
|
|
|
|
### Task 25: 创建阶段3总结文档
|
|
|
|
**Files:**
|
|
- Create: `test-framework/docs/phase3-summary.md`
|
|
|
|
**Step 1: 创建阶段3总结文档**
|
|
|
|
```markdown
|
|
# 阶段3:优化和清理 - 完成总结
|
|
|
|
## 完成的任务
|
|
|
|
1. ✅ 移除旧的scripts目录
|
|
2. ✅ 创建综合测试报告生成器
|
|
3. ✅ 创建一键测试脚本
|
|
4. ✅ 创建README文档
|
|
5. ✅ 最终验证和测试
|
|
|
|
## 创建的文件
|
|
|
|
### 工具类
|
|
- test-framework/shared/utils/reporting/TestReporter.ts
|
|
|
|
### 脚本
|
|
- test-framework/run-all-tests.sh
|
|
|
|
### 文档
|
|
- test-framework/README.md
|
|
|
|
### 报告
|
|
- test-framework/reports/html/index.html
|
|
- test-framework/reports/json/results.json
|
|
|
|
## 验证结果
|
|
|
|
- ✅ 所有测试通过
|
|
- ✅ 报告生成正常
|
|
- ✅ 文档完整
|
|
- ✅ 脚本可执行
|
|
|
|
## 最终状态
|
|
|
|
测试框架重构完成!所有功能正常工作,代码结构清晰,易于维护和扩展。
|
|
```
|
|
|
|
**Step 2: 提交**
|
|
|
|
```bash
|
|
git add test-framework/docs/phase3-summary.md
|
|
git commit -m "docs: add phase 3 completion summary"
|
|
```
|
|
|
|
---
|
|
|
|
## 总结
|
|
|
|
### 完成的所有阶段
|
|
|
|
1. ✅ **阶段1:创建共享层**
|
|
- 创建了完整的共享基础层
|
|
- 包括类型定义、配置管理、页面对象、工具类
|
|
- E2E测试成功迁移到使用共享层
|
|
|
|
2. ✅ **阶段2:迁移开发环境测试**
|
|
- 将所有开发环境测试迁移到TypeScript/Playwright
|
|
- 使用共享的页面对象和工具类
|
|
- 测试结果与原有脚本一致
|
|
|
|
3. ✅ **阶段3:优化和清理**
|
|
- 移除旧的scripts目录
|
|
- 创建综合测试报告生成器
|
|
- 创建一键测试脚本
|
|
- 完善文档
|
|
|
|
### 最终成果
|
|
|
|
- 统一的测试框架
|
|
- 完整的类型安全
|
|
- 高代码复用性
|
|
- 易于维护和扩展
|
|
- 完善的文档
|
|
|
|
### 下一步
|
|
|
|
- 将测试框架集成到CI/CD流程
|
|
- 添加更多测试覆盖
|
|
- 优化测试执行性能
|
|
- 添加测试覆盖率统计 |