Files
ai-proj-helper/skills-dev/dev-test-plugin/skills/dev-test/templates/pdv-smoke-spec.md
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

5.1 KiB
Raw Blame History

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);
  });

});