在 /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>
5.1 KiB
5.1 KiB
PDV Smoke Spec 模板
部署后验收 (Post-Deploy Verification) Playwright 测试模板。AI 根据需求变更范围,基于此模板动态生成验收 spec。
使用方式
# 指定部署环境 URL 执行
E2E_BASE_URL=https://staging.example.com npx playwright test e2e/pdv/ --project=chromium
Spec 模板结构
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 功能)
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);
});
});