Files
John Qiu 187f5621c9 feat(req): 部署门禁制度 — PDV 验收任务 + Deploy Gate 1-3
在 /req deploy 流程中增加部署后 E2E 验收(Post-Deploy Verification)门禁:
- 新增 verification linkRole 和【验收】任务命名规范
- Deploy Gate 1 健康检查 / Gate 2 PDV 任务完成 / Gate 3 证据完整
- PDV Playwright spec 模板(页面可达、菜单可见、API 连通)
- 同步更新 req-workflow、dev-test、e2e-testing 相关文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 09:34:08 +10:30

148 lines
5.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PDV Smoke Spec 模板
部署后验收 (Post-Deploy Verification) Playwright 测试模板。AI 根据需求变更范围,基于此模板动态生成验收 spec。
## 使用方式
```bash
# 指定部署环境 URL 执行
E2E_BASE_URL=https://staging.example.com npx playwright test e2e/pdv/ --project=chromium
```
## Spec 模板结构
```typescript
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:3000';
test.describe('PDV: {需求标题}', () => {
test.beforeEach(async ({ page }) => {
// 方式 1: 使用 storageState推荐需预先保存登录状态
// test.use({ storageState: 'e2e/.auth/user.json' });
// 方式 2: 手动登录
await page.goto(`${BASE_URL}/login`);
await page.fill('input[name="username"]', '{测试账号}');
await page.fill('input[name="password"]', '{测试密码}');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard/**');
});
test('菜单可见性: {菜单名}', async ({ page }) => {
await page.goto(`${BASE_URL}/`);
await page.waitForSelector('.ant-menu');
// 检查侧栏包含新菜单项
const menu = page.locator('.ant-menu');
await expect(menu).toContainText('{菜单名}');
// 截图证据
await page.screenshot({ path: 'e2e-results/pdv-menu-{菜单名}.png', fullPage: false });
});
test('页面可达: {页面路由}', async ({ page }) => {
const response = await page.goto(`${BASE_URL}{页面路由}`);
// 验证 HTTP 状态
expect(response?.status()).toBeLessThan(400);
// 验证非白屏 — title 不含错误关键词
await expect(page).not.toHaveTitle(/error|500|404|not found/i);
// 验证页面有核心内容(非空白)
await expect(page.locator('{核心选择器}')).toBeVisible({ timeout: 10000 });
// 检查无 JS 报错(通过 console error 监听)
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
await page.waitForTimeout(2000);
expect(errors.filter(e => !e.includes('favicon'))).toHaveLength(0);
// 截图证据
await page.screenshot({ path: 'e2e-results/pdv-page-{页面名}.png', fullPage: true });
});
test('API 连通: {接口描述}', async ({ request }) => {
// 需要带认证 token 调用
const resp = await request.get(`${BASE_URL}/api/v1/{路径}`, {
headers: {
'Authorization': 'Bearer {token}',
},
});
// 验证非 5xx 错误
expect(resp.status()).toBeLessThan(500);
// 可选:验证响应结构
// const body = await resp.json();
// expect(body).toHaveProperty('data');
});
});
```
## 占位符说明
| 占位符 | 含义 | 来源 |
|--------|------|------|
| `{需求标题}` | 需求名称 | `ai-proj req get --id <id>` |
| `{菜单名}` | 新增的菜单文本 | 从需求关联的前端任务中提取 |
| `{页面路由}` | 新增/变更的前端路由 | 从前端路由配置或 PRD 提取 |
| `{核心选择器}` | 页面核心内容的 CSS 选择器 | 如 `h1`, `.page-title`, `[data-testid="xxx"]` |
| `{测试账号}` / `{测试密码}` | 测试环境登录凭据 | 环境配置 |
| `{token}` | API 认证 token | 登录后获取 |
| `{接口描述}` / `{路径}` | 关键 API 端点 | 从后端路由或 PRD 提取 |
| `{页面名}` | 截图文件名标识 | 自定义 |
## 生成示例OKR 功能)
```typescript
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:3000';
test.describe('PDV: OKR 团队/对齐/设置/评分功能', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${BASE_URL}/login`);
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'TestPass123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard/**');
});
test('菜单可见性: OKR', async ({ page }) => {
await page.goto(`${BASE_URL}/`);
await page.waitForSelector('.ant-menu');
await expect(page.locator('.ant-menu')).toContainText('OKR');
await page.screenshot({ path: 'e2e-results/pdv-menu-okr.png' });
});
test('页面可达: /okr/my', async ({ page }) => {
const response = await page.goto(`${BASE_URL}/okr/my`);
expect(response?.status()).toBeLessThan(400);
await expect(page).not.toHaveTitle(/error|500|404/i);
await expect(page.locator('h1, .page-title')).toBeVisible({ timeout: 10000 });
await page.screenshot({ path: 'e2e-results/pdv-page-okr-my.png', fullPage: true });
});
test('页面可达: /okr/team', async ({ page }) => {
const response = await page.goto(`${BASE_URL}/okr/team`);
expect(response?.status()).toBeLessThan(400);
await expect(page).not.toHaveTitle(/error|500|404/i);
await expect(page.locator('h1, .page-title')).toBeVisible({ timeout: 10000 });
await page.screenshot({ path: 'e2e-results/pdv-page-okr-team.png', fullPage: true });
});
test('API 连通: OKR objectives', async ({ request }) => {
const resp = await request.get(`${BASE_URL}/api/v1/okr/objectives`);
expect(resp.status()).toBeLessThan(500);
});
});
```