/** * 结构化用例脚本化规格(自动生成,请勿手改) * ================================================================ * 来源:tests/test-cases/*.md 共 784 条 `| TC-` 表格用例 * 生成器:tests/scripts/scriptize_tc.py * 数据:./cases.json(唯一数据源,含来源文件/行号/分级/原因) * * 分级: * active-api 步骤/预期可提取明确 /api 路径 + 可判定状态码 → 真实 apiPost + code 集合断言 * active-probe 有 /api 路径但预期无状态码 → 真实 apiPost + 「不应 5xx」弱契约探针 * auth-probe 权限流派生用例,路由来自源码/文档证据 → 不带 token 访问,断言 401/403 * linked-e2e 文档已关联既有 spec → 仅作文档锚点,执行请跑原 spec * 其余 design-* 运行时 test.fixme(true, reason) 阻断,不打远程、不伪造结果 * * 路由证据:cases.json 的 route_level / route_evidence 字段记录每条路径的来源 * inline(用例原文)> inherit(派生继承)> controller(源码 Controller 名) * > doc-fuzzy(接口文档章节,模块级关联需复核)> linked-spec(既有规格实际调用) * * 授权边界(重要): * - 默认 TC_LIVE != 1:全部用例为 fixme,零网络副作用,可安全 --list / 空跑。 * - 仅显式 `TC_LIVE=1` 且已确认目标环境时,active-api 才真实请求后端。 * - 目标环境取自 playwright 配置 baseURL;执行前须由测试负责人确认可写/可压。 * * 运行: * npx playwright test e2e/api/generated/tc-scripted.spec.js -c playwright.config.api.js --list * TC_LIVE=1 npx playwright test e2e/api/generated/tc-scripted.spec.js -c playwright.config.api.js --project=desktop */ const { test, expect } = require('@playwright/test'); const { registerRole, apiPost } = require('../../helpers/api'); const CASES = require('./cases.json'); const LIVE = process.env.TC_LIVE === '1'; const _tokenCache = {}; /** 惰性注册角色会话;游客返回 null(用于未授权探针) */ async function tokenFor(request, roleKey) { if (roleKey === 'guest') return null; if (_tokenCache[roleKey]) return _tokenCache[roleKey]; const ctx = await registerRole(request, roleKey, 'QA-SCRIPT'); _tokenCache[roleKey] = ctx; return ctx; } const groups = {}; for (const tc of CASES) { (groups[tc.role || '未标注'] = groups[tc.role || '未标注'] || []).push(tc); } for (const [role, list] of Object.entries(groups)) { test.describe('脚本化用例 · ' + role + ' (' + list.length + ')', () => { for (const tc of list) { test(tc.title, async ({ request }, testInfo) => { // 把文档用例的结构化元数据注入测试报告,使设计态用例也具备可追溯性 testInfo.annotations.push( { type: 'TC', description: tc.id + ' @ ' + tc.source_file + ':' + tc.source_line }, { type: 'section', description: tc.section }, { type: 'priority', description: tc.priority }, { type: 'type', description: tc.type }, { type: 'tier', description: tc.tier }, { type: 'precondition', description: tc.precondition }, { type: 'steps', description: tc.steps }, { type: 'expected', description: tc.expected } ); const executable = tc.tier === 'active-api' || tc.tier === 'active-probe' || tc.tier === 'auth-probe'; const blocked = !executable ? (tc.reason || '设计态:缺少可自动化条件') : (LIVE ? null : '未授权联网:设置 TC_LIVE=1 且确认目标环境后执行'); if (blocked) { test.fixme(true, blocked); return; } // auth-probe 刻意不带 token:验证「未授权访问受保护接口」;其余按角色惰性注册 const ctx = tc.tier === 'auth-probe' ? null : await tokenFor(request, tc.role_key); const r = await apiPost(request, tc.api.path, tc.api.body || {}, ctx ? ctx.token : null); if (tc.tier === 'active-probe') { // 弱契约探针:预期未写状态码,只守住「不应 5xx / 非 JSON」这条底线 expect( Number(r.code) < 500, tc.id + ' 弱契约探针:不应 5xx/非 JSON,实际 code=' + r.code + ' msg=' + r.message + '(' + tc.source_file + ':' + tc.source_line + ')' ).toBeTruthy(); } else { const codes = tc.api.codes.map(Number); expect( codes.includes(Number(r.code)), tc.id + ' [' + tc.tier + '] 期望 code ∈ [' + codes.join(',') + '],实际 code=' + r.code + ' msg=' + r.message + '(' + tc.source_file + ':' + tc.source_line + ')' ).toBeTruthy(); } }); } }); }