# 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 ` | | `{菜单名}` | 新增的菜单文本 | 从需求关联的前端任务中提取 | | `{页面路由}` | 新增/变更的前端路由 | 从前端路由配置或 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); }); }); ```