042f66499a
- Add missing lucide-react icons (Users, Target, MessageCircle, Layers, CreditCard) - Fix admin/page.test.tsx ESLint errors (add displayName) - Fix api/contact/route.test.ts ESLint errors (remove any types, use import) - Add RESEND_API_KEY environment variable for API tests - All 122 test suites now passing - Test pass rate: 99.8% (1499/1502 passed, 3 skipped)
1941 lines
55 KiB
Markdown
1941 lines
55 KiB
Markdown
# 测试架构重构与User Journey测试引入计划
|
||
|
||
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
||
|
||
**目标:** 重构测试架构,消除重复代码,引入User Journey测试,提升测试质量和维护性
|
||
|
||
**架构:** 采用分层测试策略(单元测试→集成测试→E2E测试),E2E测试按职责分为smoke/journeys/features/performance/security五层,使用Page Object Model模式消除重复代码,引入User Journey测试覆盖核心业务流程
|
||
|
||
**技术栈:** Jest + React Testing Library(单元测试)、Playwright(E2E测试)、TypeScript
|
||
|
||
---
|
||
|
||
## 文件结构
|
||
|
||
### 新增文件
|
||
|
||
```
|
||
e2e/
|
||
├── pages/ # Page Object Model
|
||
│ ├── AdminLoginPage.ts # 管理员登录页面
|
||
│ ├── AdminContentPage.ts # 内容管理页面
|
||
│ ├── AdminUserPage.ts # 用户管理页面
|
||
│ ├── FrontendNewsPage.ts # 前端新闻页面
|
||
│ └── FrontendProductPage.ts # 前端产品页面
|
||
│
|
||
├── fixtures/ # 测试固件
|
||
│ ├── test-data.ts # 测试数据
|
||
│ ├── auth.ts # 认证固件
|
||
│ └── storage-state.ts # 存储状态
|
||
│
|
||
├── smoke/ # 冒烟测试(快速层)
|
||
│ ├── health-check.spec.ts # 健康检查
|
||
│ └── critical-paths.spec.ts # 关键路径
|
||
│
|
||
├── journeys/ # 用户旅程测试(标准层)
|
||
│ ├── admin-content-journey.spec.ts # 管理员内容发布旅程
|
||
│ ├── visitor-browse-journey.spec.ts # 访客浏览旅程
|
||
│ └── user-auth-journey.spec.ts # 用户认证旅程
|
||
│
|
||
├── features/ # 功能测试(标准层)
|
||
│ ├── admin/
|
||
│ │ ├── content-crud.spec.ts # 内容CRUD测试
|
||
│ │ └── user-management.spec.ts # 用户管理测试
|
||
│ └── frontend/
|
||
│ ├── responsive.spec.ts # 响应式测试
|
||
│ └── accessibility.spec.ts # 无障碍测试
|
||
│
|
||
├── performance/ # 性能测试(深度层)
|
||
│ └── page-load-performance.spec.ts # 页面加载性能
|
||
│
|
||
└── security/ # 安全测试(深度层)
|
||
├── xss-protection.spec.ts # XSS防护测试
|
||
└── auth-security.spec.ts # 认证安全测试
|
||
```
|
||
|
||
### 修改文件
|
||
|
||
```
|
||
e2e/
|
||
├── admin-publish.spec.ts # 删除(迁移到journeys和features)
|
||
├── admin-publish-core.spec.ts # 删除(迁移到journeys和features)
|
||
├── admin-frontend-interaction.spec.ts # 删除(迁移到journeys和features)
|
||
└── website-acceptance.spec.ts # 保留并优化
|
||
|
||
src/
|
||
└── components/sections/
|
||
├── news-section.integration.test.tsx # 修复导入错误
|
||
├── products-section.integration.test.tsx # 修复导入错误
|
||
└── services-section.integration.test.tsx # 修复导入错误
|
||
|
||
playwright.config.ts # 更新配置支持新目录结构
|
||
```
|
||
|
||
---
|
||
|
||
## 任务分解
|
||
|
||
### 任务 1:修复现有单元测试错误
|
||
|
||
**文件:**
|
||
- 修改:`src/components/sections/news-section.integration.test.tsx`
|
||
- 修改:`src/components/sections/products-section.integration.test.tsx`
|
||
- 修改:`src/components/sections/services-section.integration.test.tsx`
|
||
|
||
**问题分析:**
|
||
集成测试文件中导入的组件可能存在默认导出和命名导出混淆的问题。
|
||
|
||
- [ ] **步骤 1:检查NewsSection组件的导出方式**
|
||
|
||
运行:`grep -n "export" src/components/sections/news-section.tsx`
|
||
|
||
预期:确认组件是默认导出还是命名导出
|
||
|
||
- [ ] **步骤 2:修复news-section.integration.test.tsx的导入**
|
||
|
||
```typescript
|
||
// 检查当前导入
|
||
import { NewsSection } from './news-section';
|
||
|
||
// 如果是默认导出,修改为:
|
||
import NewsSection from './news-section';
|
||
|
||
// 如果组件未导出,在news-section.tsx末尾添加:
|
||
export { NewsSection };
|
||
// 或
|
||
export default NewsSection;
|
||
```
|
||
|
||
- [ ] **步骤 3:运行测试验证修复**
|
||
|
||
运行:`npm run test:unit -- src/components/sections/news-section.integration.test.tsx`
|
||
|
||
预期:PASS,所有测试通过
|
||
|
||
- [ ] **步骤 4:修复products-section.integration.test.tsx**
|
||
|
||
重复步骤1-3,修复产品组件的导入问题
|
||
|
||
- [ ] **步骤 5:修复services-section.integration.test.tsx**
|
||
|
||
重复步骤1-3,修复服务组件的导入问题
|
||
|
||
- [ ] **步骤 6:运行完整单元测试套件**
|
||
|
||
运行:`npm run test:coverage`
|
||
|
||
预期:所有测试通过,无错误
|
||
|
||
- [ ] **步骤 7:Commit**
|
||
|
||
```bash
|
||
git add src/components/sections/*.integration.test.tsx
|
||
git add src/components/sections/*.tsx
|
||
git commit -m "fix: 修复集成测试组件导入错误"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 2:创建Page Object Model基础结构
|
||
|
||
**文件:**
|
||
- 创建:`e2e/pages/AdminLoginPage.ts`
|
||
- 创建:`e2e/pages/AdminContentPage.ts`
|
||
- 创建:`e2e/pages/AdminUserPage.ts`
|
||
- 创建:`e2e/pages/FrontendNewsPage.ts`
|
||
- 创建:`e2e/pages/FrontendProductPage.ts`
|
||
|
||
- [ ] **步骤 1:创建AdminLoginPage页面对象**
|
||
|
||
```typescript
|
||
// e2e/pages/AdminLoginPage.ts
|
||
import { Page, expect } from '@playwright/test';
|
||
|
||
export class AdminLoginPage {
|
||
constructor(private page: Page) {}
|
||
|
||
async goto() {
|
||
await this.page.goto('/admin/login');
|
||
await this.page.waitForLoadState('networkidle');
|
||
}
|
||
|
||
async login(email: string, password: string) {
|
||
await this.page.fill('#email', email);
|
||
await this.page.fill('#password', password);
|
||
await this.page.click('button[type="submit"]');
|
||
await this.page.waitForURL(/\/admin(?!\/login)/);
|
||
}
|
||
|
||
async expectLoginSuccess() {
|
||
await expect(this.page).toHaveURL(/\/admin(?!\/login)/);
|
||
}
|
||
|
||
async expectLoginError() {
|
||
await expect(this.page.locator('[role="alert"]')).toBeVisible();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 2:创建AdminContentPage页面对象**
|
||
|
||
```typescript
|
||
// e2e/pages/AdminContentPage.ts
|
||
import { Page, expect } from '@playwright/test';
|
||
|
||
export interface ContentData {
|
||
type: 'news' | 'product' | 'service' | 'case';
|
||
title: string;
|
||
slug: string;
|
||
excerpt?: string;
|
||
content?: string;
|
||
category?: string;
|
||
tags?: string[];
|
||
status?: 'draft' | 'published' | 'archived';
|
||
}
|
||
|
||
export class AdminContentPage {
|
||
constructor(private page: Page) {}
|
||
|
||
async goto() {
|
||
await this.page.goto('/admin/content');
|
||
await this.page.waitForLoadState('networkidle');
|
||
}
|
||
|
||
async gotoCreate() {
|
||
await this.page.goto('/admin/content/new');
|
||
await this.page.waitForLoadState('domcontentloaded');
|
||
await this.page.waitForSelector('input[placeholder="请输入标题"]', { timeout: 60000 });
|
||
}
|
||
|
||
async createContent(data: ContentData): Promise<string | null> {
|
||
await this.gotoCreate();
|
||
|
||
await this.page.fill('input[placeholder="请输入标题"]', data.title);
|
||
await this.page.fill('input[placeholder="url-slug"]', data.slug);
|
||
|
||
if (data.excerpt) {
|
||
await this.page.fill('textarea', data.excerpt);
|
||
}
|
||
|
||
if (data.type) {
|
||
await this.page.locator('select').first().selectOption(data.type);
|
||
}
|
||
|
||
if (data.status) {
|
||
await this.page.locator('select').nth(1).selectOption(data.status);
|
||
}
|
||
|
||
if (data.category) {
|
||
await this.page.fill('input[placeholder="分类名称"]', data.category);
|
||
}
|
||
|
||
await this.page.click('button:has-text("发布")');
|
||
|
||
await this.page.waitForURL(/\/admin\/content\/[a-zA-Z0-9]+/, { timeout: 15000 });
|
||
|
||
const url = this.page.url();
|
||
const match = url.match(/\/admin\/content\/([a-zA-Z0-9]+)/);
|
||
return match ? match[1] : null;
|
||
}
|
||
|
||
async deleteContent(contentId: string) {
|
||
await this.goto();
|
||
const row = this.page.locator(`tr:has-text("${contentId}")`);
|
||
|
||
if (await row.count() > 0) {
|
||
await row.locator('button:has-text("删除")').click();
|
||
await this.page.locator('button:has-text("确认"), button:has-text("确定")').click();
|
||
await this.page.waitForResponse(resp =>
|
||
resp.url().includes('/api/admin/content') &&
|
||
resp.request().method() === 'DELETE',
|
||
{ timeout: 10000 }
|
||
);
|
||
}
|
||
}
|
||
|
||
async expectContentInList(title: string) {
|
||
await this.goto();
|
||
const row = this.page.locator(`tr:has-text("${title}")`);
|
||
await expect(row).toBeVisible();
|
||
}
|
||
|
||
async expectContentNotInList(title: string) {
|
||
await this.goto();
|
||
const row = this.page.locator(`tr:has-text("${title}")`);
|
||
await expect(row).not.toBeVisible();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 3:创建AdminUserPage页面对象**
|
||
|
||
```typescript
|
||
// e2e/pages/AdminUserPage.ts
|
||
import { Page, expect } from '@playwright/test';
|
||
|
||
export interface UserData {
|
||
email: string;
|
||
password: string;
|
||
name?: string;
|
||
role?: 'admin' | 'editor' | 'viewer';
|
||
}
|
||
|
||
export class AdminUserPage {
|
||
constructor(private page: Page) {}
|
||
|
||
async goto() {
|
||
await this.page.goto('/admin/users');
|
||
await this.page.waitForLoadState('networkidle');
|
||
}
|
||
|
||
async createUser(data: UserData) {
|
||
await this.page.click('button:has-text("新建用户")');
|
||
await this.page.fill('input[name="email"]', data.email);
|
||
await this.page.fill('input[name="password"]', data.password);
|
||
|
||
if (data.name) {
|
||
await this.page.fill('input[name="name"]', data.name);
|
||
}
|
||
|
||
if (data.role) {
|
||
await this.page.selectOption('select[name="role"]', data.role);
|
||
}
|
||
|
||
await this.page.click('button[type="submit"]');
|
||
}
|
||
|
||
async expectUserInList(email: string) {
|
||
await this.goto();
|
||
const row = this.page.locator(`tr:has-text("${email}")`);
|
||
await expect(row).toBeVisible();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 4:创建FrontendNewsPage页面对象**
|
||
|
||
```typescript
|
||
// e2e/pages/FrontendNewsPage.ts
|
||
import { Page, expect } from '@playwright/test';
|
||
|
||
export class FrontendNewsPage {
|
||
constructor(private page: Page) {}
|
||
|
||
async goto() {
|
||
await this.page.goto('/news');
|
||
await this.page.waitForLoadState('networkidle');
|
||
}
|
||
|
||
async expectNewsVisible(title: string) {
|
||
const newsCard = this.page.locator(`text="${title}"`);
|
||
await expect(newsCard).toBeVisible();
|
||
}
|
||
|
||
async expectNewsNotVisible(title: string) {
|
||
const newsCard = this.page.locator(`text="${title}"`);
|
||
await expect(newsCard).not.toBeVisible();
|
||
}
|
||
|
||
async clickNews(title: string) {
|
||
await this.page.locator(`text="${title}"`).click();
|
||
await this.page.waitForLoadState('networkidle');
|
||
}
|
||
|
||
async expectNewsDetailVisible(content: string) {
|
||
await expect(this.page.locator(`text=${content}`)).toBeVisible();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 5:创建FrontendProductPage页面对象**
|
||
|
||
```typescript
|
||
// e2e/pages/FrontendProductPage.ts
|
||
import { Page, expect } from '@playwright/test';
|
||
|
||
export class FrontendProductPage {
|
||
constructor(private page: Page) {}
|
||
|
||
async goto() {
|
||
await this.page.goto('/products');
|
||
await this.page.waitForLoadState('networkidle');
|
||
}
|
||
|
||
async expectProductVisible(title: string) {
|
||
const productCard = this.page.locator(`text="${title}"`);
|
||
await expect(productCard).toBeVisible();
|
||
}
|
||
|
||
async clickProduct(title: string) {
|
||
await this.page.locator(`text="${title}"`).click();
|
||
await this.page.waitForLoadState('networkidle');
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 6:创建pages目录索引文件**
|
||
|
||
```typescript
|
||
// e2e/pages/index.ts
|
||
export { AdminLoginPage } from './AdminLoginPage';
|
||
export { AdminContentPage, type ContentData } from './AdminContentPage';
|
||
export { AdminUserPage, type UserData } from './AdminUserPage';
|
||
export { FrontendNewsPage } from './FrontendNewsPage';
|
||
export { FrontendProductPage } from './FrontendProductPage';
|
||
```
|
||
|
||
- [ ] **步骤 7:Commit**
|
||
|
||
```bash
|
||
git add e2e/pages/
|
||
git commit -m "feat: 创建Page Object Model基础结构"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 3:创建测试固件
|
||
|
||
**文件:**
|
||
- 创建:`e2e/fixtures/test-data.ts`
|
||
- 创建:`e2e/fixtures/auth.ts`
|
||
- 创建:`e2e/fixtures/storage-state.ts`
|
||
|
||
- [ ] **步骤 1:创建测试数据固件**
|
||
|
||
```typescript
|
||
// e2e/fixtures/test-data.ts
|
||
export const testFixtures = {
|
||
adminUser: {
|
||
email: process.env.ADMIN_EMAIL || 'admin@novalon.cn',
|
||
password: process.env.ADMIN_PASSWORD || 'admin123456',
|
||
},
|
||
|
||
testContent: {
|
||
news: {
|
||
type: 'news' as const,
|
||
title: `测试新闻-${Date.now()}`,
|
||
slug: `test-news-${Date.now()}`,
|
||
excerpt: '这是一条测试新闻的摘要内容',
|
||
content: '<p>这是测试新闻的正文内容</p>',
|
||
category: '公司新闻',
|
||
tags: ['测试', '自动化'],
|
||
status: 'published' as const,
|
||
},
|
||
product: {
|
||
type: 'product' as const,
|
||
title: `测试产品-${Date.now()}`,
|
||
slug: `test-product-${Date.now()}`,
|
||
excerpt: '这是一个测试产品的描述',
|
||
content: '<p>测试产品的详细介绍</p>',
|
||
category: '软件产品',
|
||
tags: ['产品', '测试'],
|
||
status: 'published' as const,
|
||
},
|
||
service: {
|
||
type: 'service' as const,
|
||
title: `测试服务-${Date.now()}`,
|
||
slug: `test-service-${Date.now()}`,
|
||
excerpt: '这是一个测试服务的描述',
|
||
content: '<p>测试服务的详细介绍</p>',
|
||
category: '软件开发',
|
||
tags: ['服务', '测试'],
|
||
status: 'published' as const,
|
||
},
|
||
case: {
|
||
type: 'case' as const,
|
||
title: `测试案例-${Date.now()}`,
|
||
slug: `test-case-${Date.now()}`,
|
||
excerpt: '这是一个测试案例的描述',
|
||
content: '<p>测试案例的详细介绍</p>',
|
||
category: '企业服务',
|
||
tags: ['案例', '测试'],
|
||
status: 'published' as const,
|
||
},
|
||
},
|
||
|
||
invalidContent: {
|
||
empty: {
|
||
type: 'news' as const,
|
||
title: '',
|
||
slug: '',
|
||
content: '',
|
||
},
|
||
xss: {
|
||
type: 'news' as const,
|
||
title: `XSS测试-${Date.now()}`,
|
||
slug: `xss-test-${Date.now()}`,
|
||
excerpt: '<script>alert("XSS")</script>测试摘要',
|
||
content: '<p><script>alert("XSS")</script>测试内容</p>',
|
||
category: '安全测试',
|
||
tags: ['安全'],
|
||
status: 'published' as const,
|
||
},
|
||
},
|
||
};
|
||
```
|
||
|
||
- [ ] **步骤 2:创建认证固件**
|
||
|
||
```typescript
|
||
// e2e/fixtures/auth.ts
|
||
import { test as base } from '@playwright/test';
|
||
import { AdminLoginPage } from '../pages/AdminLoginPage';
|
||
import { testFixtures } from './test-data';
|
||
|
||
type AuthFixtures = {
|
||
authenticatedPage: void;
|
||
adminLoginPage: AdminLoginPage;
|
||
};
|
||
|
||
export const test = base.extend<AuthFixtures>({
|
||
authenticatedPage: async ({ page }, use) => {
|
||
const loginPage = new AdminLoginPage(page);
|
||
await loginPage.goto();
|
||
await loginPage.login(testFixtures.adminUser.email, testFixtures.adminUser.password);
|
||
await loginPage.expectLoginSuccess();
|
||
|
||
await use();
|
||
},
|
||
|
||
adminLoginPage: async ({ page }, use) => {
|
||
await use(new AdminLoginPage(page));
|
||
},
|
||
});
|
||
|
||
export { expect } from '@playwright/test';
|
||
```
|
||
|
||
- [ ] **步骤 3:创建存储状态固件**
|
||
|
||
```typescript
|
||
// e2e/fixtures/storage-state.ts
|
||
import { test as base } from '@playwright/test';
|
||
import path from 'path';
|
||
|
||
const AUTH_FILE = path.join(__dirname, '../.auth/admin.json');
|
||
|
||
type StorageStateFixtures = {
|
||
adminStorageState: string;
|
||
};
|
||
|
||
export const test = base.extend<StorageStateFixtures>({
|
||
adminStorageState: async ({ browser }, use) => {
|
||
const context = await browser.newContext();
|
||
const page = await context.newPage();
|
||
|
||
await page.goto('/admin/login');
|
||
await page.fill('#email', process.env.ADMIN_EMAIL || 'admin@novalon.cn');
|
||
await page.fill('#password', process.env.ADMIN_PASSWORD || 'admin123456');
|
||
await page.click('button[type="submit"]');
|
||
await page.waitForURL(/\/admin(?!\/login)/);
|
||
|
||
await page.context().storageState({ path: AUTH_FILE });
|
||
await context.close();
|
||
|
||
await use(AUTH_FILE);
|
||
},
|
||
});
|
||
|
||
export { expect } from '@playwright/test';
|
||
```
|
||
|
||
- [ ] **步骤 4:创建fixtures目录索引文件**
|
||
|
||
```typescript
|
||
// e2e/fixtures/index.ts
|
||
export { testFixtures } from './test-data';
|
||
export { test as authTest, expect } from './auth';
|
||
export { test as storageStateTest } from './storage-state';
|
||
```
|
||
|
||
- [ ] **步骤 5:Commit**
|
||
|
||
```bash
|
||
git add e2e/fixtures/
|
||
git commit -m "feat: 创建测试固件和数据管理"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 4:创建冒烟测试(快速层)
|
||
|
||
**文件:**
|
||
- 创建:`e2e/smoke/health-check.spec.ts`
|
||
- 创建:`e2e/smoke/critical-paths.spec.ts`
|
||
|
||
- [ ] **步骤 1:创建健康检查测试**
|
||
|
||
```typescript
|
||
// e2e/smoke/health-check.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('健康检查 @smoke @critical', () => {
|
||
test('应用能够正常启动', async ({ page }) => {
|
||
await page.goto('/');
|
||
await expect(page).toHaveTitle(/四川睿新致远科技有限公司/);
|
||
});
|
||
|
||
test('健康检查API正常', async ({ request }) => {
|
||
const response = await request.get('/api/health');
|
||
expect(response.status()).toBe(200);
|
||
|
||
const body = await response.json();
|
||
expect(body.status).toBe('ok');
|
||
});
|
||
|
||
test('静态资源可访问', async ({ request }) => {
|
||
const response = await request.get('/favicon.svg');
|
||
expect(response.status()).toBe(200);
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:创建关键路径测试**
|
||
|
||
```typescript
|
||
// e2e/smoke/critical-paths.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
import { testFixtures } from '../fixtures/test-data';
|
||
|
||
test.describe('关键路径测试 @smoke @critical', () => {
|
||
test('首页加载正常', async ({ page }) => {
|
||
await page.goto('/');
|
||
|
||
await expect(page.locator('header')).toBeVisible();
|
||
await expect(page.locator('footer')).toBeVisible();
|
||
await expect(page.locator('nav')).toBeVisible();
|
||
});
|
||
|
||
test('管理员能够登录', async ({ page }) => {
|
||
await page.goto('/admin/login');
|
||
await page.fill('#email', testFixtures.adminUser.email);
|
||
await page.fill('#password', testFixtures.adminUser.password);
|
||
await page.click('button[type="submit"]');
|
||
|
||
await expect(page).toHaveURL(/\/admin(?!\/login)/);
|
||
});
|
||
|
||
test('新闻页面可访问', async ({ page }) => {
|
||
await page.goto('/news');
|
||
await expect(page).toHaveURL(/\/news/);
|
||
await expect(page.locator('header')).toBeVisible();
|
||
});
|
||
|
||
test('产品页面可访问', async ({ page }) => {
|
||
await page.goto('/products');
|
||
await expect(page).toHaveURL(/\/products/);
|
||
await expect(page.locator('header')).toBeVisible();
|
||
});
|
||
|
||
test('联系页面可访问', async ({ page }) => {
|
||
await page.goto('/contact');
|
||
await expect(page).toHaveURL(/\/contact/);
|
||
await expect(page.locator('form')).toBeVisible();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 3:Commit**
|
||
|
||
```bash
|
||
git add e2e/smoke/
|
||
git commit -m "feat: 创建冒烟测试(快速层)"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 5:创建User Journey测试 - 管理员内容发布旅程
|
||
|
||
**文件:**
|
||
- 创建:`e2e/journeys/admin-content-journey.spec.ts`
|
||
|
||
- [ ] **步骤 1:创建管理员内容发布旅程测试**
|
||
|
||
```typescript
|
||
// e2e/journeys/admin-content-journey.spec.ts
|
||
import { test, expect } from '../fixtures/auth';
|
||
import { AdminContentPage, FrontendNewsPage, FrontendProductPage } from '../pages';
|
||
import { testFixtures } from '../fixtures/test-data';
|
||
|
||
test.describe('管理员内容发布完整旅程 @journey @admin', () => {
|
||
let contentPage: AdminContentPage;
|
||
let newsPage: FrontendNewsPage;
|
||
let productPage: FrontendProductPage;
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
contentPage = new AdminContentPage(page);
|
||
newsPage = new FrontendNewsPage(page);
|
||
productPage = new FrontendProductPage(page);
|
||
});
|
||
|
||
test('管理员发布新闻并验证用户可见性', async ({ page, authenticatedPage }) => {
|
||
const testNews = testFixtures.testContent.news;
|
||
let contentId: string | null = null;
|
||
|
||
await test.step('步骤1: 管理员创建新闻内容', async () => {
|
||
contentId = await contentPage.createContent(testNews);
|
||
expect(contentId).not.toBeNull();
|
||
});
|
||
|
||
await test.step('步骤2: 验证后台列表显示', async () => {
|
||
await contentPage.expectContentInList(testNews.title);
|
||
});
|
||
|
||
await test.step('步骤3: 验证前端用户可见', async () => {
|
||
await newsPage.goto();
|
||
await newsPage.expectNewsVisible(testNews.title);
|
||
});
|
||
|
||
await test.step('步骤4: 用户查看新闻详情', async () => {
|
||
await newsPage.clickNews(testNews.title);
|
||
await newsPage.expectNewsDetailVisible(testNews.excerpt!);
|
||
});
|
||
|
||
await test.step('步骤5: 验证SEO元数据', async () => {
|
||
const title = await page.title();
|
||
expect(title).toContain(testNews.title);
|
||
});
|
||
|
||
await test.step('步骤6: 清理测试数据', async () => {
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
await contentPage.expectContentNotInList(testNews.title);
|
||
}
|
||
});
|
||
});
|
||
|
||
test('管理员发布产品并验证用户可见性', async ({ page, authenticatedPage }) => {
|
||
const testProduct = testFixtures.testContent.product;
|
||
let contentId: string | null = null;
|
||
|
||
await test.step('步骤1: 管理员创建产品内容', async () => {
|
||
contentId = await contentPage.createContent(testProduct);
|
||
expect(contentId).not.toBeNull();
|
||
});
|
||
|
||
await test.step('步骤2: 验证前端用户可见', async () => {
|
||
await productPage.goto();
|
||
await productPage.expectProductVisible(testProduct.title);
|
||
});
|
||
|
||
await test.step('步骤3: 清理测试数据', async () => {
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
});
|
||
|
||
test('管理员保存草稿并验证前端不可见', async ({ page, authenticatedPage }) => {
|
||
const draftContent = {
|
||
...testFixtures.testContent.news,
|
||
status: 'draft' as const,
|
||
title: `草稿测试-${Date.now()}`,
|
||
slug: `draft-test-${Date.now()}`,
|
||
};
|
||
|
||
let contentId: string | null = null;
|
||
|
||
await test.step('步骤1: 管理员保存草稿', async () => {
|
||
contentId = await contentPage.createContent(draftContent);
|
||
expect(contentId).not.toBeNull();
|
||
});
|
||
|
||
await test.step('步骤2: 验证前端用户不可见', async () => {
|
||
await newsPage.goto();
|
||
await newsPage.expectNewsNotVisible(draftContent.title);
|
||
});
|
||
|
||
await test.step('步骤3: 清理测试数据', async () => {
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
});
|
||
|
||
test('管理员编辑已发布内容并验证更新', async ({ page, authenticatedPage }) => {
|
||
const testNews = testFixtures.testContent.news;
|
||
let contentId: string | null = null;
|
||
|
||
await test.step('步骤1: 创建初始内容', async () => {
|
||
contentId = await contentPage.createContent(testNews);
|
||
expect(contentId).not.toBeNull();
|
||
});
|
||
|
||
await test.step('步骤2: 编辑内容', async () => {
|
||
await page.goto(`/admin/content/${contentId}`);
|
||
await page.waitForLoadState('domcontentloaded');
|
||
|
||
const updatedTitle = `${testNews.title}-已修改`;
|
||
await page.fill('input[placeholder="请输入标题"]', updatedTitle);
|
||
await page.click('button:has-text("保存草稿")');
|
||
|
||
await page.waitForResponse(resp =>
|
||
resp.url().includes(`/api/admin/content/${contentId}`) &&
|
||
resp.request().method() === 'PUT',
|
||
{ timeout: 15000 }
|
||
);
|
||
});
|
||
|
||
await test.step('步骤3: 验证前端更新', async () => {
|
||
await newsPage.goto();
|
||
await newsPage.expectNewsVisible(`${testNews.title}-已修改`);
|
||
});
|
||
|
||
await test.step('步骤4: 清理测试数据', async () => {
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add e2e/journeys/admin-content-journey.spec.ts
|
||
git commit -m "feat: 创建管理员内容发布User Journey测试"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 6:创建User Journey测试 - 访客浏览旅程
|
||
|
||
**文件:**
|
||
- 创建:`e2e/journeys/visitor-browse-journey.spec.ts`
|
||
|
||
- [ ] **步骤 1:创建访客浏览旅程测试**
|
||
|
||
```typescript
|
||
// e2e/journeys/visitor-browse-journey.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('访客浏览完整旅程 @journey @visitor', () => {
|
||
test('访客从首页浏览到联系表单提交', async ({ page }) => {
|
||
await test.step('步骤1: 访问首页', async () => {
|
||
await page.goto('/');
|
||
await expect(page).toHaveTitle(/四川睿新致远科技有限公司/);
|
||
await expect(page.locator('header')).toBeVisible();
|
||
});
|
||
|
||
await test.step('步骤2: 浏览产品列表', async () => {
|
||
await page.click('a[href="/products"]');
|
||
await page.waitForLoadState('networkidle');
|
||
await expect(page).toHaveURL(/\/products/);
|
||
|
||
const productCards = page.locator('article, .card, [class*="product"]');
|
||
const count = await productCards.count();
|
||
expect(count).toBeGreaterThan(0);
|
||
});
|
||
|
||
await test.step('步骤3: 查看产品详情', async () => {
|
||
const firstProduct = page.locator('a[href*="/products/"]').first();
|
||
if (await firstProduct.count() > 0) {
|
||
await firstProduct.click();
|
||
await page.waitForLoadState('networkidle');
|
||
await expect(page.locator('main, article')).toBeVisible();
|
||
}
|
||
});
|
||
|
||
await test.step('步骤4: 浏览案例列表', async () => {
|
||
await page.goto('/cases');
|
||
await page.waitForLoadState('networkidle');
|
||
await expect(page).toHaveURL(/\/cases/);
|
||
});
|
||
|
||
await test.step('步骤5: 查看案例详情', async () => {
|
||
const firstCase = page.locator('a[href*="/cases/"]').first();
|
||
if (await firstCase.count() > 0) {
|
||
await firstCase.click();
|
||
await page.waitForLoadState('networkidle');
|
||
await expect(page.locator('main, article')).toBeVisible();
|
||
}
|
||
});
|
||
|
||
await test.step('步骤6: 提交咨询表单', async () => {
|
||
await page.goto('/contact');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
await page.fill('input[name="name"]', '测试用户');
|
||
await page.fill('input[name="phone"]', '13800138000');
|
||
await page.fill('input[name="email"]', 'test@example.com');
|
||
await page.fill('textarea[name="message"]', '这是一条测试咨询信息');
|
||
|
||
await page.click('button[type="submit"]');
|
||
|
||
await expect(page.locator('text=/提交成功|感谢您的咨询/')).toBeVisible({ timeout: 10000 });
|
||
});
|
||
});
|
||
|
||
test('访客浏览新闻并查看详情', async ({ page }) => {
|
||
await test.step('步骤1: 访问新闻列表', async () => {
|
||
await page.goto('/news');
|
||
await page.waitForLoadState('networkidle');
|
||
await expect(page).toHaveURL(/\/news/);
|
||
});
|
||
|
||
await test.step('步骤2: 查看新闻详情', async () => {
|
||
const firstNews = page.locator('a[href*="/news/"]').first();
|
||
if (await firstNews.count() > 0) {
|
||
await firstNews.click();
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
await expect(page.locator('main, article')).toBeVisible();
|
||
|
||
const title = await page.title();
|
||
expect(title.length).toBeGreaterThan(0);
|
||
}
|
||
});
|
||
|
||
await test.step('步骤3: 验证页面SEO', async () => {
|
||
const metaDesc = await page.locator('meta[name="description"]').getAttribute('content');
|
||
expect(metaDesc).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
test('访客响应式浏览体验', async ({ page }) => {
|
||
const viewports = [
|
||
{ name: '移动端', width: 375, height: 667 },
|
||
{ name: '平板端', width: 768, height: 1024 },
|
||
{ name: '桌面端', width: 1920, height: 1080 },
|
||
];
|
||
|
||
for (const viewport of viewports) {
|
||
await test.step(`${viewport.name}浏览`, async () => {
|
||
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
||
await page.goto('/');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
await expect(page.locator('header')).toBeVisible();
|
||
await expect(page.locator('footer')).toBeVisible();
|
||
|
||
await page.goto('/news');
|
||
await page.waitForLoadState('networkidle');
|
||
await expect(page.locator('header')).toBeVisible();
|
||
});
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add e2e/journeys/visitor-browse-journey.spec.ts
|
||
git commit -m "feat: 创建访客浏览User Journey测试"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 7:创建User Journey测试 - 用户认证旅程
|
||
|
||
**文件:**
|
||
- 创建:`e2e/journeys/user-auth-journey.spec.ts`
|
||
|
||
- [ ] **步骤 1:创建用户认证旅程测试**
|
||
|
||
```typescript
|
||
// e2e/journeys/user-auth-journey.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
import { AdminLoginPage } from '../pages';
|
||
import { testFixtures } from '../fixtures/test-data';
|
||
|
||
test.describe('用户认证完整旅程 @journey @auth', () => {
|
||
test('管理员登录登出完整流程', async ({ page }) => {
|
||
const loginPage = new AdminLoginPage(page);
|
||
|
||
await test.step('步骤1: 访问登录页面', async () => {
|
||
await loginPage.goto();
|
||
await expect(page.locator('form')).toBeVisible();
|
||
});
|
||
|
||
await test.step('步骤2: 输入错误密码验证失败', async () => {
|
||
await loginPage.login(testFixtures.adminUser.email, 'wrongpassword');
|
||
await loginPage.expectLoginError();
|
||
});
|
||
|
||
await test.step('步骤3: 输入正确密码登录成功', async () => {
|
||
await loginPage.login(testFixtures.adminUser.email, testFixtures.adminUser.password);
|
||
await loginPage.expectLoginSuccess();
|
||
});
|
||
|
||
await test.step('步骤4: 访问后台管理页面', async () => {
|
||
await page.goto('/admin/content');
|
||
await page.waitForLoadState('networkidle');
|
||
await expect(page.locator('table')).toBeVisible();
|
||
});
|
||
|
||
await test.step('步骤5: 登出', async () => {
|
||
await page.click('button:has-text("退出"), a:has-text("退出")');
|
||
await page.waitForURL(/\/admin\/login/);
|
||
await expect(page).toHaveURL(/\/admin\/login/);
|
||
});
|
||
|
||
await test.step('步骤6: 验证登出后无法访问后台', async () => {
|
||
await page.goto('/admin/content');
|
||
await page.waitForURL(/\/admin\/login/, { timeout: 5000 });
|
||
await expect(page).toHaveURL(/\/admin\/login/);
|
||
});
|
||
});
|
||
|
||
test('未登录用户访问后台重定向到登录页', async ({ page }) => {
|
||
await test.step('访问后台内容管理页面', async () => {
|
||
await page.goto('/admin/content');
|
||
await page.waitForURL(/\/admin\/login/, { timeout: 5000 });
|
||
await expect(page).toHaveURL(/\/admin\/login/);
|
||
});
|
||
|
||
await test.step('访问后台用户管理页面', async () => {
|
||
await page.goto('/admin/users');
|
||
await page.waitForURL(/\/admin\/login/, { timeout: 5000 });
|
||
await expect(page).toHaveURL(/\/admin\/login/);
|
||
});
|
||
});
|
||
|
||
test('API权限验证', async ({ request }) => {
|
||
await test.step('未授权访问管理API返回403', async () => {
|
||
const response = await request.post('/api/admin/content', {
|
||
data: {
|
||
type: 'news',
|
||
title: '未授权测试',
|
||
slug: 'unauthorized-test',
|
||
content: '测试内容',
|
||
},
|
||
});
|
||
|
||
expect([401, 403]).toContain(response.status());
|
||
});
|
||
|
||
await test.step('未授权访问用户管理API返回403', async () => {
|
||
const response = await request.get('/api/admin/users');
|
||
expect([401, 403]).toContain(response.status());
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add e2e/journeys/user-auth-journey.spec.ts
|
||
git commit -m "feat: 创建用户认证User Journey测试"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 8:创建功能测试 - 内容管理
|
||
|
||
**文件:**
|
||
- 创建:`e2e/features/admin/content-crud.spec.ts`
|
||
|
||
- [ ] **步骤 1:创建内容CRUD功能测试**
|
||
|
||
```typescript
|
||
// e2e/features/admin/content-crud.spec.ts
|
||
import { test, expect } from '../../fixtures/auth';
|
||
import { AdminContentPage } from '../../pages';
|
||
import { testFixtures } from '../../fixtures/test-data';
|
||
|
||
test.describe('内容管理CRUD功能测试 @admin @content', () => {
|
||
let contentPage: AdminContentPage;
|
||
|
||
test.beforeEach(async ({ page, authenticatedPage }) => {
|
||
contentPage = new AdminContentPage(page);
|
||
});
|
||
|
||
test('创建新闻内容', async ({ page }) => {
|
||
const testNews = testFixtures.testContent.news;
|
||
const contentId = await contentPage.createContent(testNews);
|
||
|
||
expect(contentId).not.toBeNull();
|
||
await contentPage.expectContentInList(testNews.title);
|
||
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
|
||
test('创建产品内容', async ({ page }) => {
|
||
const testProduct = testFixtures.testContent.product;
|
||
const contentId = await contentPage.createContent(testProduct);
|
||
|
||
expect(contentId).not.toBeNull();
|
||
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
|
||
test('创建服务内容', async ({ page }) => {
|
||
const testService = testFixtures.testContent.service;
|
||
const contentId = await contentPage.createContent(testService);
|
||
|
||
expect(contentId).not.toBeNull();
|
||
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
|
||
test('创建案例内容', async ({ page }) => {
|
||
const testCase = testFixtures.testContent.case;
|
||
const contentId = await contentPage.createContent(testCase);
|
||
|
||
expect(contentId).not.toBeNull();
|
||
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
|
||
test('空内容提交验证', async ({ page }) => {
|
||
await contentPage.gotoCreate();
|
||
await page.click('button:has-text("发布")');
|
||
|
||
const errorMessage = page.locator('text=/请输入标题|标题不能为空|请输入|必填/');
|
||
await expect(errorMessage.first()).toBeVisible();
|
||
});
|
||
|
||
test('删除内容', async ({ page }) => {
|
||
const testNews = testFixtures.testContent.news;
|
||
const contentId = await contentPage.createContent(testNews);
|
||
|
||
expect(contentId).not.toBeNull();
|
||
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
await contentPage.expectContentNotInList(testNews.title);
|
||
}
|
||
});
|
||
|
||
test('归档内容', async ({ page }) => {
|
||
const testNews = testFixtures.testContent.news;
|
||
const contentId = await contentPage.createContent(testNews);
|
||
|
||
expect(contentId).not.toBeNull();
|
||
|
||
if (contentId) {
|
||
await page.goto(`/admin/content/${contentId}`);
|
||
await page.waitForLoadState('domcontentloaded');
|
||
|
||
await page.locator('select').nth(1).selectOption('archived');
|
||
await page.click('button:has-text("保存草稿")');
|
||
|
||
await page.waitForResponse(resp =>
|
||
resp.url().includes(`/api/admin/content/${contentId}`) &&
|
||
resp.request().method() === 'PUT',
|
||
{ timeout: 15000 }
|
||
);
|
||
|
||
await contentPage.goto();
|
||
const row = page.locator(`tr:has-text("${testNews.title}")`);
|
||
await expect(row.locator('td:has-text("已归档")')).toBeVisible();
|
||
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add e2e/features/admin/content-crud.spec.ts
|
||
git commit -m "feat: 创建内容管理CRUD功能测试"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 9:创建功能测试 - 用户管理
|
||
|
||
**文件:**
|
||
- 创建:`e2e/features/admin/user-management.spec.ts`
|
||
|
||
- [ ] **步骤 1:创建用户管理功能测试**
|
||
|
||
```typescript
|
||
// e2e/features/admin/user-management.spec.ts
|
||
import { test, expect } from '../../fixtures/auth';
|
||
import { AdminUserPage } from '../../pages';
|
||
|
||
test.describe('用户管理功能测试 @admin @user', () => {
|
||
let userPage: AdminUserPage;
|
||
|
||
test.beforeEach(async ({ page, authenticatedPage }) => {
|
||
userPage = new AdminUserPage(page);
|
||
});
|
||
|
||
test('用户列表加载', async ({ page }) => {
|
||
await userPage.goto();
|
||
|
||
const table = page.locator('table');
|
||
await expect(table).toBeVisible();
|
||
|
||
const rows = page.locator('tbody tr');
|
||
const count = await rows.count();
|
||
expect(count).toBeGreaterThanOrEqual(0);
|
||
});
|
||
|
||
test('创建新用户', async ({ page }) => {
|
||
const testUser = {
|
||
email: `test-${Date.now()}@example.com`,
|
||
password: 'Test123456!',
|
||
name: '测试用户',
|
||
role: 'editor' as const,
|
||
};
|
||
|
||
await userPage.createUser(testUser);
|
||
await userPage.expectUserInList(testUser.email);
|
||
});
|
||
|
||
test('用户权限验证', async ({ page }) => {
|
||
await userPage.goto();
|
||
await expect(page.locator('table')).toBeVisible();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add e2e/features/admin/user-management.spec.ts
|
||
git commit -m "feat: 创建用户管理功能测试"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 10:创建功能测试 - 前端响应式和无障碍
|
||
|
||
**文件:**
|
||
- 创建:`e2e/features/frontend/responsive.spec.ts`
|
||
- 创建:`e2e/features/frontend/accessibility.spec.ts`
|
||
|
||
- [ ] **步骤 1:创建响应式测试**
|
||
|
||
```typescript
|
||
// e2e/features/frontend/responsive.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('响应式设计测试 @frontend @responsive', () => {
|
||
test('移动端首页显示', async ({ page }) => {
|
||
await page.setViewportSize({ width: 375, height: 667 });
|
||
await page.goto('/');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
await expect(page.locator('header')).toBeVisible();
|
||
await expect(page.locator('footer')).toBeVisible();
|
||
|
||
const menuButton = page.locator('button[aria-label*="菜单"], button[class*="menu"]');
|
||
const hasMenuButton = await menuButton.count();
|
||
|
||
if (hasMenuButton > 0) {
|
||
await menuButton.first().click();
|
||
await page.waitForTimeout(500);
|
||
}
|
||
});
|
||
|
||
test('平板端首页显示', async ({ page }) => {
|
||
await page.setViewportSize({ width: 768, height: 1024 });
|
||
await page.goto('/');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
await expect(page.locator('header')).toBeVisible();
|
||
await expect(page.locator('footer')).toBeVisible();
|
||
});
|
||
|
||
test('桌面端首页显示', async ({ page }) => {
|
||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||
await page.goto('/');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
await expect(page.locator('header')).toBeVisible();
|
||
await expect(page.locator('footer')).toBeVisible();
|
||
|
||
const navLinks = page.locator('nav a');
|
||
const count = await navLinks.count();
|
||
expect(count).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('各页面响应式布局', async ({ page }) => {
|
||
const pages = [
|
||
{ url: '/news', name: '新闻' },
|
||
{ url: '/products', name: '产品' },
|
||
{ url: '/services', name: '服务' },
|
||
{ url: '/cases', name: '案例' },
|
||
];
|
||
|
||
for (const p of pages) {
|
||
await page.setViewportSize({ width: 375, height: 667 });
|
||
await page.goto(p.url);
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
await expect(page.locator('header')).toBeVisible();
|
||
await expect(page.locator('footer')).toBeVisible();
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:创建无障碍测试**
|
||
|
||
```typescript
|
||
// e2e/features/frontend/accessibility.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
import AxeBuilder from '@axe-core/playwright';
|
||
|
||
test.describe('无障碍测试 @frontend @accessibility', () => {
|
||
test('首页无障碍检查', async ({ page }) => {
|
||
await page.goto('/');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
|
||
|
||
expect(accessibilityScanResults.violations).toEqual([]);
|
||
});
|
||
|
||
test('新闻页面无障碍检查', async ({ page }) => {
|
||
await page.goto('/news');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
|
||
|
||
const criticalViolations = accessibilityScanResults.violations.filter(
|
||
violation => violation.impact === 'critical' || violation.impact === 'serious'
|
||
);
|
||
|
||
expect(criticalViolations).toEqual([]);
|
||
});
|
||
|
||
test('联系页面无障碍检查', async ({ page }) => {
|
||
await page.goto('/contact');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
|
||
|
||
const criticalViolations = accessibilityScanResults.violations.filter(
|
||
violation => violation.impact === 'critical' || violation.impact === 'serious'
|
||
);
|
||
|
||
expect(criticalViolations).toEqual([]);
|
||
});
|
||
|
||
test('页面语言属性', async ({ page }) => {
|
||
await page.goto('/');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
const htmlLang = await page.locator('html').getAttribute('lang');
|
||
expect(htmlLang).toBeTruthy();
|
||
});
|
||
|
||
test('图片alt属性', async ({ page }) => {
|
||
await page.goto('/');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
const images = page.locator('img');
|
||
const count = await images.count();
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const alt = await images.nth(i).getAttribute('alt');
|
||
expect(alt).toBeDefined();
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 3:Commit**
|
||
|
||
```bash
|
||
git add e2e/features/frontend/
|
||
git commit -m "feat: 创建前端响应式和无障碍测试"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 11:创建性能测试
|
||
|
||
**文件:**
|
||
- 创建:`e2e/performance/page-load-performance.spec.ts`
|
||
|
||
- [ ] **步骤 1:创建页面加载性能测试**
|
||
|
||
```typescript
|
||
// e2e/performance/page-load-performance.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('页面加载性能测试 @performance', () => {
|
||
test('首页加载性能', async ({ page }) => {
|
||
const startTime = Date.now();
|
||
await page.goto('/');
|
||
await page.waitForLoadState('networkidle');
|
||
const loadTime = Date.now() - startTime;
|
||
|
||
console.log(`首页加载时间: ${loadTime}ms`);
|
||
expect(loadTime).toBeLessThan(5000);
|
||
});
|
||
|
||
test('各页面加载时间', async ({ page }) => {
|
||
const pages = [
|
||
{ url: '/', name: '首页' },
|
||
{ url: '/news', name: '新闻' },
|
||
{ url: '/products', name: '产品' },
|
||
{ url: '/services', name: '服务' },
|
||
{ url: '/cases', name: '案例' },
|
||
];
|
||
|
||
for (const p of pages) {
|
||
const startTime = Date.now();
|
||
await page.goto(p.url);
|
||
await page.waitForLoadState('networkidle');
|
||
const loadTime = Date.now() - startTime;
|
||
|
||
console.log(`${p.name}页面加载时间: ${loadTime}ms`);
|
||
expect(loadTime).toBeLessThan(5000);
|
||
}
|
||
});
|
||
|
||
test('后台列表加载性能', async ({ page }) => {
|
||
await page.goto('/admin/login');
|
||
await page.fill('#email', process.env.ADMIN_EMAIL || 'admin@novalon.cn');
|
||
await page.fill('#password', process.env.ADMIN_PASSWORD || 'admin123456');
|
||
await page.click('button[type="submit"]');
|
||
await page.waitForURL(/\/admin(?!\/login)/);
|
||
|
||
const startTime = Date.now();
|
||
await page.goto('/admin/content');
|
||
await page.waitForLoadState('networkidle');
|
||
const loadTime = Date.now() - startTime;
|
||
|
||
console.log(`后台列表加载时间: ${loadTime}ms`);
|
||
expect(loadTime).toBeLessThan(3000);
|
||
});
|
||
|
||
test('API响应时间', async ({ request }) => {
|
||
const startTime = Date.now();
|
||
const response = await request.get('/api/health');
|
||
const responseTime = Date.now() - startTime;
|
||
|
||
console.log(`API响应时间: ${responseTime}ms`);
|
||
expect(responseTime).toBeLessThan(1000);
|
||
expect(response.status()).toBe(200);
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add e2e/performance/
|
||
git commit -m "feat: 创建页面加载性能测试"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 12:创建安全测试
|
||
|
||
**文件:**
|
||
- 创建:`e2e/security/xss-protection.spec.ts`
|
||
- 创建:`e2e/security/auth-security.spec.ts`
|
||
|
||
- [ ] **步骤 1:创建XSS防护测试**
|
||
|
||
```typescript
|
||
// e2e/security/xss-protection.spec.ts
|
||
import { test, expect } from '../fixtures/auth';
|
||
import { AdminContentPage } from '../pages';
|
||
import { testFixtures } from '../fixtures/test-data';
|
||
|
||
test.describe('XSS防护测试 @security @xss', () => {
|
||
let contentPage: AdminContentPage;
|
||
|
||
test.beforeEach(async ({ page, authenticatedPage }) => {
|
||
contentPage = new AdminContentPage(page);
|
||
});
|
||
|
||
test('XSS攻击防护 - 标题字段', async ({ page }) => {
|
||
const xssContent = testFixtures.invalidContent.xss;
|
||
const contentId = await contentPage.createContent(xssContent);
|
||
|
||
expect(contentId).not.toBeNull();
|
||
|
||
await page.goto('/news');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
const xssTriggered = await page.evaluate(() => {
|
||
return (window as any).xssTriggered === true;
|
||
});
|
||
|
||
expect(xssTriggered).toBe(false);
|
||
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
|
||
test('XSS攻击防护 - 内容字段', async ({ page }) => {
|
||
const xssContent = {
|
||
...testFixtures.testContent.news,
|
||
content: '<script>alert("XSS")</script><p>正常内容</p>',
|
||
};
|
||
|
||
const contentId = await contentPage.createContent(xssContent);
|
||
expect(contentId).not.toBeNull();
|
||
|
||
await page.goto('/news');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
const xssTriggered = await page.evaluate(() => {
|
||
return (window as any).xssTriggered === true;
|
||
});
|
||
|
||
expect(xssTriggered).toBe(false);
|
||
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:创建认证安全测试**
|
||
|
||
```typescript
|
||
// e2e/security/auth-security.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
import { testFixtures } from '../fixtures/test-data';
|
||
|
||
test.describe('认证安全测试 @security @auth', () => {
|
||
test('SQL注入防护 - 登录表单', async ({ page }) => {
|
||
await page.goto('/admin/login');
|
||
|
||
await page.fill('#email', "admin' OR '1'='1");
|
||
await page.fill('#password', "password' OR '1'='1");
|
||
await page.click('button[type="submit"]');
|
||
|
||
await expect(page).toHaveURL(/\/admin\/login/);
|
||
});
|
||
|
||
test('暴力破解防护', async ({ page }) => {
|
||
await page.goto('/admin/login');
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
await page.fill('#email', testFixtures.adminUser.email);
|
||
await page.fill('#password', `wrongpassword${i}`);
|
||
await page.click('button[type="submit"]');
|
||
|
||
await page.waitForTimeout(500);
|
||
}
|
||
|
||
await expect(page).toHaveURL(/\/admin\/login/);
|
||
});
|
||
|
||
test('会话过期验证', async ({ page }) => {
|
||
await page.goto('/admin/login');
|
||
await page.fill('#email', testFixtures.adminUser.email);
|
||
await page.fill('#password', testFixtures.adminUser.password);
|
||
await page.click('button[type="submit"]');
|
||
await page.waitForURL(/\/admin(?!\/login)/);
|
||
|
||
await page.context().clearCookies();
|
||
|
||
await page.goto('/admin/content');
|
||
await page.waitForURL(/\/admin\/login/, { timeout: 5000 });
|
||
await expect(page).toHaveURL(/\/admin\/login/);
|
||
});
|
||
|
||
test('CSRF防护', async ({ request }) => {
|
||
const response = await request.post('/api/admin/content', {
|
||
data: {
|
||
type: 'news',
|
||
title: 'CSRF测试',
|
||
slug: 'csrf-test',
|
||
content: '测试内容',
|
||
},
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
});
|
||
|
||
expect([401, 403, 500]).toContain(response.status());
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 3:Commit**
|
||
|
||
```bash
|
||
git add e2e/security/
|
||
git commit -m "feat: 创建安全测试"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 13:更新Playwright配置
|
||
|
||
**文件:**
|
||
- 修改:`playwright.config.ts`
|
||
|
||
- [ ] **步骤 1:更新Playwright配置支持新目录结构**
|
||
|
||
```typescript
|
||
// playwright.config.ts
|
||
import { defineConfig, devices } from '@playwright/test';
|
||
|
||
const isCI = !!process.env.CI;
|
||
const testTier = (process.env.TEST_TIER || 'standard') as 'fast' | 'standard' | 'deep';
|
||
const baseURL = process.env.BASE_URL || (isCI ? 'http://localhost:3000' : 'https://novalon.cn');
|
||
|
||
const tierConfig: Record<'fast' | 'standard' | 'deep', {
|
||
timeout: number;
|
||
retries: number;
|
||
workers: number | undefined;
|
||
}> = {
|
||
fast: {
|
||
timeout: 15000,
|
||
retries: 0,
|
||
workers: 2,
|
||
},
|
||
standard: {
|
||
timeout: 30000,
|
||
retries: isCI ? 1 : 0,
|
||
workers: isCI ? 1 : undefined,
|
||
},
|
||
deep: {
|
||
timeout: 60000,
|
||
retries: 2,
|
||
workers: 1,
|
||
},
|
||
};
|
||
|
||
const config = tierConfig[testTier];
|
||
|
||
export default defineConfig({
|
||
testDir: './e2e',
|
||
testMatch: [
|
||
'**/*.spec.ts',
|
||
'**/*.test.ts',
|
||
],
|
||
fullyParallel: !isCI,
|
||
forbidOnly: isCI,
|
||
retries: config.retries,
|
||
workers: config.workers,
|
||
timeout: config.timeout,
|
||
reporter: isCI
|
||
? [
|
||
['html', { outputFolder: 'reports/html', open: 'never' }],
|
||
['json', { outputFile: 'reports/results.json' }],
|
||
['list']
|
||
]
|
||
: 'html',
|
||
use: {
|
||
baseURL,
|
||
trace: 'on-first-retry',
|
||
screenshot: 'only-on-failure',
|
||
video: 'retain-on-failure',
|
||
launchOptions: isCI ? {
|
||
args: ['--disable-dev-shm-usage', '--no-sandbox']
|
||
} : undefined,
|
||
},
|
||
webServer: isCI ? {
|
||
command: 'npm run start',
|
||
port: 3000,
|
||
timeout: 120000,
|
||
reuseExistingServer: false,
|
||
} : undefined,
|
||
projects: isCI
|
||
? [
|
||
{
|
||
name: 'smoke',
|
||
testMatch: /smoke\/.*\.spec\.ts/,
|
||
use: { ...devices['Desktop Chrome'] },
|
||
},
|
||
{
|
||
name: 'journeys',
|
||
testMatch: /journeys\/.*\.spec\.ts/,
|
||
use: { ...devices['Desktop Chrome'] },
|
||
},
|
||
{
|
||
name: 'features',
|
||
testMatch: /features\/.*\.spec\.ts/,
|
||
use: { ...devices['Desktop Chrome'] },
|
||
},
|
||
]
|
||
: [
|
||
{
|
||
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'] },
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add playwright.config.ts
|
||
git commit -m "feat: 更新Playwright配置支持新目录结构"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 14:删除旧的E2E测试文件
|
||
|
||
**文件:**
|
||
- 删除:`e2e/admin-publish.spec.ts`
|
||
- 删除:`e2e/admin-publish-core.spec.ts`
|
||
- 删除:`e2e/admin-frontend-interaction.spec.ts`
|
||
|
||
- [ ] **步骤 1:备份旧测试文件(可选)**
|
||
|
||
```bash
|
||
mkdir -p e2e/.archive
|
||
mv e2e/admin-publish.spec.ts e2e/.archive/
|
||
mv e2e/admin-publish-core.spec.ts e2e/.archive/
|
||
mv e2e/admin-frontend-interaction.spec.ts e2e/.archive/
|
||
```
|
||
|
||
- [ ] **步骤 2:删除旧测试文件**
|
||
|
||
```bash
|
||
rm e2e/admin-publish.spec.ts
|
||
rm e2e/admin-publish-core.spec.ts
|
||
rm e2e/admin-frontend-interaction.spec.ts
|
||
```
|
||
|
||
- [ ] **步骤 3:Commit**
|
||
|
||
```bash
|
||
git add -A e2e/
|
||
git commit -m "refactor: 删除旧的E2E测试文件,迁移到新架构"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 15:更新package.json测试脚本
|
||
|
||
**文件:**
|
||
- 修改:`package.json`
|
||
|
||
- [ ] **步骤 1:更新测试脚本**
|
||
|
||
```json
|
||
{
|
||
"scripts": {
|
||
"test": "playwright test",
|
||
"test:unit": "jest",
|
||
"test:coverage": "jest --coverage",
|
||
"test:coverage:check": "jest --coverage --ci",
|
||
"test:e2e": "playwright test",
|
||
"test:smoke": "TEST_TIER=fast playwright test --project=smoke",
|
||
"test:journeys": "playwright test --project=journeys",
|
||
"test:features": "playwright test --project=features",
|
||
"test:fast": "TEST_TIER=fast playwright test",
|
||
"test:standard": "TEST_TIER=standard playwright test",
|
||
"test:deep": "TEST_TIER=deep playwright test"
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add package.json
|
||
git commit -m "feat: 更新测试脚本支持新架构"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 16:运行完整测试套件验证
|
||
|
||
**文件:**
|
||
- 无文件修改
|
||
|
||
- [ ] **步骤 1:运行单元测试**
|
||
|
||
运行:`npm run test:coverage`
|
||
|
||
预期:所有单元测试通过
|
||
|
||
- [ ] **步骤 2:运行冒烟测试**
|
||
|
||
运行:`npm run test:smoke`
|
||
|
||
预期:所有冒烟测试通过
|
||
|
||
- [ ] **步骤 3:运行User Journey测试**
|
||
|
||
运行:`npm run test:journeys`
|
||
|
||
预期:所有旅程测试通过
|
||
|
||
- [ ] **步骤 4:运行功能测试**
|
||
|
||
运行:`npm run test:features`
|
||
|
||
预期:所有功能测试通过
|
||
|
||
- [ ] **步骤 5:运行完整E2E测试套件**
|
||
|
||
运行:`npm run test:e2e`
|
||
|
||
预期:所有E2E测试通过
|
||
|
||
- [ ] **步骤 6:生成测试报告**
|
||
|
||
运行:`npm run test:e2e -- --reporter=html`
|
||
|
||
预期:生成HTML测试报告
|
||
|
||
---
|
||
|
||
### 任务 17:更新测试文档
|
||
|
||
**文件:**
|
||
- 创建:`docs/testing/user-journey-testing-guide.md`
|
||
|
||
- [ ] **步骤 1:创建User Journey测试指南**
|
||
|
||
```markdown
|
||
# User Journey测试指南
|
||
|
||
## 概述
|
||
|
||
User Journey测试(用户旅程测试)是从用户视角出发,模拟真实用户完成某个业务目标的完整流程测试。
|
||
|
||
## 核心概念
|
||
|
||
### 什么是User Journey测试?
|
||
|
||
User Journey测试关注的是"用户如何使用系统",而不是"系统有哪些功能"。它模拟真实用户的行为路径,验证整个业务流程的流畅性。
|
||
|
||
### 与功能测试的区别
|
||
|
||
| 维度 | 功能测试 | User Journey测试 |
|
||
|------|---------|-----------------|
|
||
| 视角 | 系统功能视角 | 用户行为视角 |
|
||
| 范围 | 单个功能点 | 完整业务流程 |
|
||
| 数据 | 每次创建新数据 | 复用上下文数据 |
|
||
| 目标 | 验证功能正确性 | 验证用户体验流畅性 |
|
||
|
||
## 编写规范
|
||
|
||
### 1. 使用test.step组织步骤
|
||
|
||
\`\`\`typescript
|
||
test('管理员发布新闻旅程', async ({ page }) => {
|
||
await test.step('步骤1: 登录', async () => {
|
||
// 登录逻辑
|
||
});
|
||
|
||
await test.step('步骤2: 创建内容', async () => {
|
||
// 创建逻辑
|
||
});
|
||
|
||
await test.step('步骤3: 验证展示', async () => {
|
||
// 验证逻辑
|
||
});
|
||
});
|
||
\`\`\`
|
||
|
||
### 2. 使用Page Object Model
|
||
|
||
\`\`\`typescript
|
||
const loginPage = new AdminLoginPage(page);
|
||
await loginPage.goto();
|
||
await loginPage.login(email, password);
|
||
await loginPage.expectLoginSuccess();
|
||
\`\`\`
|
||
|
||
### 3. 清理测试数据
|
||
|
||
\`\`\`typescript
|
||
test.afterEach(async () => {
|
||
if (contentId) {
|
||
await contentPage.deleteContent(contentId);
|
||
}
|
||
});
|
||
\`\`\`
|
||
|
||
## 最佳实践
|
||
|
||
1. **从用户视角思考**:模拟真实用户的行为路径
|
||
2. **保持测试独立**:每个旅程测试应该独立运行
|
||
3. **清理测试数据**:测试结束后清理创建的数据
|
||
4. **使用有意义的断言**:验证用户关心的结果
|
||
5. **记录测试步骤**:使用test.step提高可读性
|
||
|
||
## 示例
|
||
|
||
参见 `e2e/journeys/` 目录下的测试文件。
|
||
```
|
||
|
||
- [ ] **步骤 2:Commit**
|
||
|
||
```bash
|
||
git add docs/testing/user-journey-testing-guide.md
|
||
git commit -m "docs: 创建User Journey测试指南"
|
||
```
|
||
|
||
---
|
||
|
||
## 自检清单
|
||
|
||
### 1. 规格覆盖度
|
||
|
||
- [x] 修复现有单元测试错误
|
||
- [x] 消除E2E测试重复代码
|
||
- [x] 引入User Journey测试
|
||
- [x] 重构测试架构
|
||
- [x] 创建Page Object Model
|
||
- [x] 创建测试固件
|
||
- [x] 创建分层测试(smoke/journeys/features/performance/security)
|
||
- [x] 更新Playwright配置
|
||
- [x] 更新测试文档
|
||
|
||
### 2. 占位符扫描
|
||
|
||
- [x] 无"待定"、"TODO"、"后续实现"等占位符
|
||
- [x] 所有代码步骤都包含完整代码
|
||
- [x] 所有命令都包含具体命令和预期输出
|
||
|
||
### 3. 类型一致性
|
||
|
||
- [x] Page Object Model中的方法签名一致
|
||
- [x] 测试固件中的类型定义一致
|
||
- [x] 测试数据结构一致
|
||
|
||
---
|
||
|
||
## 执行选项
|
||
|
||
计划已完成并保存到 `docs/plans/2026-04-09-test-architecture-refactoring.md`。两种执行方式:
|
||
|
||
**1. 子代理驱动(推荐)** - 每个任务调度一个新的子代理,任务间进行审查,快速迭代
|
||
|
||
**2. 内联执行** - 在当前会话中使用 executing-plans 执行任务,批量执行并设有检查点
|
||
|
||
**选哪种方式?**
|
||
|
||
**如果选择子代理驱动:**
|
||
- **必需子技能:** 使用 superpowers:subagent-driven-development
|
||
- 每个任务一个新子代理 + 两阶段审查
|
||
|
||
**如果选择内联执行:**
|
||
- **必需子技能:** 使用 superpowers:executing-plans
|
||
- 批量执行并设有检查点供审查
|