automated-e2e-testing
将手动测试用例转为 Playwright E2E 测试并执行时使用;含写自动化前的业务熟悉踩点、Page Object/Helper 编写、执行中的 Bug 证据收集与报告条目记录。不用于:纯 API 接口测试(api-testing)、以理解系统为目的的独立探索会话(exploratory-testing)、已确认 Bug 的根因分析(bug-analysis)。
Install
npx skills add https://github.com/fishzjp/qa-skills/tree/main/skills/automated-e2e-testing
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install fishzjp-qa-skills@llmmart
git clone https://github.com/fishzjp/qa-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole fishzjp/qa-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
自动化 E2E 测试(automated-e2e-testing)
本 skill 覆盖 Web 应用自动化测试的完整工作流:将手动测试用例(markmap + Schema)转化为 Playwright spec → 运行并验证 → 发现 Bug → 输出测试报告。
核心原则:
- 先熟悉业务再写测试 — 对功能不熟悉时,先用自动化脚本主动探索系统,理解实际行为后再动手写测试代码
- 有疑问就提问,不自行假设 — 编写过程中遇到任何不明确的地方,必须向用户提问澄清,绝不凭猜测写代码
- 每条自动化用例对应一条手动用例,每条 test 只测一个点
- 每个 test 独立(自建数据 + 自清理)
- 必须使用 Page Object — 正式测试中禁止裸写定位器,所有页面交互封装在 Page Object 中
工程约定(脚手架、配置、场景代码模板、Page Object 规范)统一在 references/playwright-conventions.md——写代码时加载;通用 Helper(登录、多会话、证据收集)的参考实现在 references/helpers_reference.md;类型域三类执行片段(a11y 扫描 / 视觉基线 / 多浏览器矩阵)见工程约定第 12–14 节,type_scope 判入对应轴时按档加载取用
When to Use
- 给定测试用例(markmap / Schema),需要生成 Playwright spec 文件并执行
- 编写自动化前,需要小规模业务熟悉探索(踩点页面结构、提取选择器)
- 自动化执行中发现 Bug,需要收集证据并记录测试报告条目
- 需要编写新的 Page Object 或 Helper 函数
When NOT to Use
- 端到端测试整个需求(理解→策略→用例→执行→报告的流水线)→ 用
qaskill 编排 - 编写手动测试用例 → 用
test-case-writingskill - 纯 API 接口测试(无 Web UI 流程)→ 用
api-testingskill - 以理解系统 / 发现风险为目的的独立探索式测试会话(charter 驱动、产出探索笔记)→ 用
exploratory-testingskill;本 skill 的工作流零只做「为写自动化踩点」的小规模探索 - 已确认 Bug 的根因定位、影响分析、回归建议 → 用
bug-analysisskill;本 skill 只负责收集 Bug 证据(截图/API/控制台)并记录报告条目 - 代码变更后判断回归范围 → 用
regression-testingskill - 单元测试 → 用 Jest/Vitest;性能压测 → 专业工具(k6、locust);安全测试 → 安全审计专项(见
test-strategy的 handoff 约定)
提问时机(必须遵守)
核心规则:不确定就问,宁可多问不要瞎猜。 格式与裁决规则统一按 ../core/clarify-pattern.md(场景用「执行确认」)。
| 场景 | 应提问的内容 | 不要自行假设 |
|---|---|---|
| 元素定位失败 | "在{页面}上找不到{元素},实际页面结构是否与预期一致?" | 不要随意换选择器猜测 |
| 操作路径不明确 | "测试用例说{操作X},但页面上没有直接的入口" | 不要自行拼凑操作步骤 |
| 预期行为有歧义 | "预期{结果A},实际{结果B},应以哪个为准?" | 不要选择性地相信其中一个 |
| 业务规则不清楚 | "规则的具体边界是什么?" | 不要用常见默认值代替 |
| 探索中发现异常 | "发现{异常行为},这是预期行为还是 Bug?" | 不要自行判定是 Bug 还是特性 |
| 用例反复超时/不稳定 | "{页面}是否存在长连接或轮询推送(WebSocket/SSE/心跳上报)导致页面永不空闲?" | 不要一律套 networkidle 等待,按等待降级阶梯处理 |
工作流零:业务熟悉(前置必做,为写自动化踩点的小规模探索)
何时需要:从未测试过该功能模块 / 出现不熟悉的页面路由 / 需要编写新的 Page Object / 拿到用例但不知道系统长什么样
本工作流是小规模踩点探索(理解页面结构、提取选择器、落 Page Object),产出服务于工作流一。以理解系统 / 发现风险为目的的完整探索会话(charter 驱动、产出探索笔记)用
exploratory-testingskill。
步骤
- 探索页面结构:登录 → 导航到目标页面 → 截图 → 枚举所有可交互元素
- 体验核心流程:按测试用例步骤走一遍完整流程,每步截图,记录 API 调用
- 记录发现:页面导航路径、关键元素选择器、API 接口、隐藏行为、编写或更新 Page Object
探索代码模板见 references/playwright-conventions.md 第 4 节(explore-*.spec.ts)。
完成标准
- 每个涉及的页面都有截图
- 理解了页面间的导航路径
- 确认了关键元素的选择器
- 记录了实际的 API 请求
- 发现了隐藏行为(隐藏字段、默认值、前置条件)
- 已编写或更新了对应的 Page Object
工作流一:手动用例 → 自动化代码
前提:已完成工作流零(业务熟悉),对目标功能有充分认知。
用例输入与自动化范围判定
输入是 test-case-writing 产出的 markmap 用例文件(如含 测试用例.schema.yaml 则一并消费):
- Schema 的
execution_model: ui且automation.supported != no的用例 → 本工作流的转换对象 execution_model: dev-collab(无 UI 协作用例)→ 移交api-testing或保持手动协作执行,不硬造 UI 自动化automation_plan存在时(test-strategy产出)按用户的执行策略裁决执行;策略未定时向用户确认哪些用例转自动化
步骤
- 解析测试用例:从 markmap Markdown 提取场景与预期;test 名沿用用例的 TC 编号(如
TC-01-03: {用例名称}),手动用例缺编号时先补编号再转换 - 选择/新建 Page Object:检查
tests/pages/下是否已有对应 Page Object,优先复用 - 编写 spec:使用 Page Object 封装所有页面交互。每条 test 收笔前过断言三问:① 前置里有"不应匹配/应被排除"的数据吗?断言覆盖了"不在"吗?② 被测步骤的触发方式与用例规格一致吗(不得为绕开不稳定换触发路径)?③ 失败时能区分"功能坏了"还是"环境没加载"吗?——判定与反例见工程约定第 15 节
- 运行验证:
npx playwright test tests/{文件名}.spec.ts;基线零通过禁交付——首轮运行 0 条通过即视为流程认知错误(选择器/弹窗结构/等待假设与真实页面不符):回工作流零针对性重踩核实,修订复跑至 ≥1 条通过后才可进入后续步骤,禁止交付基线零通过的规格
脚手架、场景代码模板(CRUD / 表单提交 / 状态流转 / 多用户并发)、Page Object 规范与 spec 命名此时加载 references/playwright-conventions.md(第 1、5–7 节)。
工作流二:Bug 探索与记录
在已理解业务逻辑的前提下,系统性验证页面功能,发现 Bug 和不一致。
批量失败前置分流:一轮执行结束失败 ≥3 条时,先加载
../core/triage.md做四分类定类(A 真缺陷 / B 资产问题 / C 环境 / D 不稳定),仅 A 类进入本工作流的证据收集与报告条目;<3 条维持单条流程不变。headless 流水线场景(PR 冒烟 / 夜间全量 / 发布卡点):检查点降级为未决项、产物落盘规范与退出码语义按
../core/pipeline-integration.md(此时加载);其回流闭环中的批量失败同样进入上方分流流程。
职责边界:本工作流负责发现 + 证据收集 + 报告条目。Bug 被确认后的根因定位、影响分析、回归建议移交
bug-analysisskill(对应报告条目中根因分析等五个扩展字段留 TODO)。
Bug 发现策略
| 策略 | 检查方式 | 典型 Bug |
|---|---|---|
| UI 预期不一致 | 对比测试用例与实际截图 | 默认值不对、文案错误 |
| 表单校验缺失 | 提交非法数据 | 必填不校验、范围不限制 |
| 网络异常 | 监听 API 响应 | 500 错误、超时 |
| 数据不一致 | 操作后 reload 验证 | 提交成功但数据丢失 |
| 状态流转错误 | 多步操作验证状态 | 状态未更新、卡死 |
| 控制台错误 | setupConsoleLogging(page) 持续监听 |
组件崩溃、静默失败 |
Bug 证据收集(发现 Bug 时必须执行)
Bug 发现 → 截图操作前 → 操作触发异常 → 截图操作后 → 收集 API/控制台日志 → 写入报告
证据收集代码(setupBugTracking / tracker.collect)见 references/playwright-conventions.md 第 8 节;通用 Helper 落地时加载 references/helpers_reference.md。
截图命名统一小写 bug-{序号}-* 前缀:手动截图记录过程两态(bug-001-01-操作前.png、bug-001-02-异常.png);tracker.collect() 自动补一张整页汇总截图(bug-001-汇总.png)并 attach 到 HTML 报告。Bug 报告标题中的 BUG-{序号} 仅为显示格式。
Bug 报告格式
记录在 {项目名}/测试报告_{来源}_{日期}.md,条目字段与 ../core/report-template.md §3(测试报告唯一来源模板,此时加载)保持一致,保证条目可直接拼装进 qa 收尾的最终报告;本 skill 的发现方式含「业务熟悉探索」。执行分报告还需包含 §2 执行统计(P0/P1/P2 × 用例数/通过/失败/阻塞/未执行表,同样按 report-template);条目中根因分析等五个扩展字段留 TODO,由 bug-analysis 填写。按 report-template「机读摘要」约定,分报告末节追加机读摘要片段(占位符替换后),供 qa 收尾与后续回归编排机读解析。
Explore 文件生命周期管理
业务熟悉探索文件(explore-xxx.spec.ts)是临时产物,遵循以下生命周期:
创建 explore-xxx.spec.ts → 提取知识到 Page Object → 删除 explore 文件 + 截图
规则
- 知识提取后立即删除:将发现的选择器、API 路径、页面结构写入 Page Object 或 helpers 后,删除 explore 文件
- 截图清理:探索产生的
debug-*.png截图是临时调试产物,确认不需要后删除 - 不积累:
tests/目录下不应存在已完成的 explore 文件。如果存在,说明知识提取步骤被跳过了 - Bug 报告截图保留:
bug-{序号}-*.png截图属于 Bug 报告的一部分,不删除
关键技巧速查表
下表方法名(
createDualSession/tracker.collect/setupBugTracking/isServerCrash等)为本 skill 脚手架内置 helper 的示例(定义在references/),不是 Playwright 通用 API——换项目时按references/helpers_reference.md适配实际脚手架,勿假定这些函数存在。
| 场景 | 方法 | 备注 |
|---|---|---|
| 受控组件输入框(Ant Design 等) | fill() 优先,不生效再 keyboard.type() |
fill 会派发 input 事件,多数受控组件可用;keyboard.type 逐键最稳但慢 |
| 确认弹窗按钮 | 在 role=dialog 作用域内定位(如 page.getByRole('dialog').getByRole('button', { name: '确定' })) |
防止与页面同名按钮歧义命中;弹窗外「确定/删除」同名按钮是常见误击源 |
| 隐藏必填字段 | page.evaluate() 直接设值 |
阻止表单提交的常见原因 |
| 网络请求验证 | page.on('request') 监听 |
必须在操作之前设置 |
| 数据持久化验证 | page.reload() + 断言 |
确认数据真正保存 |
| 多用户会话 | createDualSession(browser, roleA, roleB) |
两个角色并发 |
| 登录态复用 | createSession(browser, role) |
缓存 storageState 免重复 UI 登录(工程约定第 10 节) |
| 等待策略 | ① auto-wait + expect 断言 → ② waitForResponse → ③ networkidle(仅传统 SSR/MPA) |
自上而下优先(工程约定第 9 节);固定 waitForTimeout 仅临时调试;长轮询/心跳页对 networkidle 永不空闲 |
| 并行执行 | 默认串行,独立性达标后开 workers |
前提:自建数据 + 自清理 + 唯一命名逐项核对(工程约定第 11 节) |
| flaky 定性 | 首次失败原样重跑通过 → 判 flaky | 重跑只用于定性;定位根因前标 test.fixme(工程约定第 11 节) |
| 截图调试 | page.screenshot({ fullPage: true }) |
每个关键步骤都截图 |
| 服务端崩溃检测 | isServerCrash(bodyText) |
检测 500/502 错误 |
| 唯一命名 | ${前缀}-${Date.now()} |
避免测试间名称冲突 |
| Bug 证据收集 | tracker.collect(testInfo, id, desc) |
一键收集截图+API+控制台 |
| Bug 探索前置 | setupBugTracking(page) |
注册 API 和控制台监听 |
Common Mistakes
| 错误 | 后果 | 正确做法 |
|---|---|---|
受控组件值不生效仍反复 fill() |
测试卡死或断言失败 | fill 失效时改用 keyboard.type()(见速查表) |
| 网络监听放在 click 之后 | 捕捉不到请求 | 先注册 listener,再执行操作 |
| 不清理测试数据 | 后续测试受影响 | afterEach 中删除创建的资源 |
| 用固定 timeout 等待 | 时序不稳定 | 按等待降级阶梯换用:① auto-wait+expect 断言 → ② waitForResponse → ③ networkidle(工程约定第 9 节) |
| 不验证持久化 | 数据可能只存在内存 | reload 后重新断言 |
| 正式测试不使用 Page Object | 代码重复,难以维护 | 所有页面交互封装在 Page Object 中 |
| explore 文件不删除 | 上下文污染,AI 每次读大量废代码 | 知识提取后立即删除 |
| 遇到疑问自行假设 | 产出不可靠的测试 | 不确定就向用户提问 |
| Bug 只截图不记录 API/控制台 | 开发无法定位根因 | 用 setupBugTracking() + tracker.collect() |
| 硬编码环境地址/账号 | 无法跨环境运行、泄露敏感信息 | 统一放 constants.ts / .env,代码只引用常量 |
| 失败重跑变绿就当没事 | flaky 混入主干,CI 随机红 | 按 flaky 定性流程处理:先定性再修根因,禁止调大重试硬压(工程约定第 11 节) |
| 对长轮询/推送页强套 networkidle | 超时假失败 | 按等待降级阶梯换 waitForResponse/断言自动轮询(工程约定第 9 节) |
| 每个 context 都走一遍登录页 | 执行时长翻倍、缓存认证态形同虚设 | 用 createSession() 复用 storageState,失效自动回退 UI 登录(工程约定第 10 节) |
| 所有用例永久串行不敢并行 | 全量执行时长线性膨胀 | 独立性核对通过后渐进开启 workers(工程约定第 11 节) |
| 手动用例信息不足仍硬造定位器与操作路径 | 幻觉自动化:断言全绿但没测到真实行为 | 先过 ../core/executability.md 转换闸门 + 工作流零踩点核实页面结构 |
与其他 skill 配合
- 上游:
test-case-writing产出手动用例(markmap + Schema)→ 本 skill 把execution_model: ui且可自动化的用例变成可执行代码;端到端流水线由qa编排 - 旁路:无文档 / 系统陌生的完整探索会话用
exploratory-testing,其探索笔记可作为本 skill 踩点的输入 - 下游:Bug 确认后的根因 / 影响 / 回归分析 →
bug-analysis;执行报告与 Bug 条目按../core/report-template.md对齐,供qa收尾汇总 - 平级:纯 API 用例的自动化 →
api-testing
Files (qa-skills)
-
references
-
helpers_reference.md 9.5 KB
# 通用 Helper 参考实现(脱敏) > **路径口径**:本文档内 `../core/...`、`references/...` 等相对路径按消费方 SKILL.md 所在目录(`skills/automated-e2e-testing/`)解析书写,不是相对本文件。 落地 `tests/helpers.ts` 时可直接复制以下代码作为起点,按你的系统调整选择器、登录后跳转规则和 token 存储位置。 > 业务专有的"造数"函数(如创建某类业务实体并灌入测试数据)请基于你的 Page Object 自行实现,不要塞进通用 helpers。 ```typescript // tests/helpers.ts import * as fs from 'node:fs'; import { Page, Locator, Request, Browser, BrowserContext, TestInfo } from '@playwright/test'; import { BASE_URL, ACCOUNTS } from './constants'; type Role = keyof typeof ACCOUNTS; type ApiEntry = { method: string; url: string; status?: number; body?: string }; /** 登录页 Page Object。按你的系统调整选择器和登录后跳转规则。 */ export class LoginPage { readonly page: Page; readonly usernameInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; constructor(page: Page) { this.page = page; this.usernameInput = page.getByPlaceholder('请输入用户名'); // 按实际页面调整 this.passwordInput = page.getByPlaceholder('请输入密码'); this.submitButton = page.getByRole('button', { name: /登\s*录/ }); } async goto() { await this.page.goto(`${BASE_URL}/<登录页路由>`); // 表单元素由后续 fill/keyboard 的 auto-wait 兜底(阶梯①);勿在此套 networkidle } /** 按角色登录,角色名对应 constants.ts 中 ACCOUNTS 的 key */ async loginAs(role: Role) { const { username, password } = ACCOUNTS[role]; await this.goto(); // 登录失败难兜底,为最大兼容用 keyboard.type;常规表单 fill 优先(见 SKILL.md 速查表) await this.usernameInput.click(); await this.page.keyboard.type(username, { delay: 30 }); await this.passwordInput.click(); await this.page.keyboard.type(password, { delay: 30 }); // 登录后等待跳转离开登录页(按你的系统调整预期 URL) await Promise.all([ this.page.waitForURL(/\/(dashboard|home|task-center)/, { timeout: 15000 }), this.submitButton.click(), ]); } } /** 创建两个独立会话(各自登录指定角色),用于多用户并发场景 */ export async function createDualSession(browser: Browser, roleA: Role, roleB: Role) { const contextA = await browser.newContext(); const contextB = await browser.newContext(); const pageA = await contextA.newPage(); const pageB = await contextB.newPage(); await new LoginPage(pageA).loginAs(roleA); await new LoginPage(pageB).loginAs(roleB); return { pageA, pageB, contextA, contextB }; } /** 创建 N 个角色的会话(多角色场景) */ export async function createMultiSession(browser: Browser, roles: Role[]) { const sessions: Array<{ page: Page; context: BrowserContext; role: string }> = []; for (const role of roles) { const context = await browser.newContext(); const page = await context.newPage(); await new LoginPage(page).loginAs(role); sessions.push({ page, context, role }); } return sessions; } /** 安全关闭若干浏览器上下文(忽略已关闭的) */ export function closeContexts(...contexts: (BrowserContext | null)[]) { return Promise.all( contexts.filter((c): c is BrowserContext => !!c).map(c => c.close().catch(() => {})), ); } /** 认证态缓存目录。含登录凭据,务必加入 .gitignore(见 references/playwright-conventions.md 第 10 节) */ const AUTH_DIR = '.auth'; /** 把当前已登录页面的认证态落盘为 .auth/{role}.json,返回缓存文件路径 */ export async function saveLoginState(page: Page, role: Role) { fs.mkdirSync(AUTH_DIR, { recursive: true }); const file = `${AUTH_DIR}/${role}.json`; await page.context().storageState({ path: file }); return file; } /** 创建已登录会话:优先复用 storageState 缓存;缺失/失效时回退 UI 登录并刷新缓存。 * 每个用例独立创建会话,替代裸 createMultiSession 的全量 UI 登录,显著缩短执行时长 */ export async function createSession(browser: Browser, role: Role) { const file = `${AUTH_DIR}/${role}.json`; let session: { page: Page; context: BrowserContext } | null = null; if (fs.existsSync(file)) { const context = await browser.newContext({ storageState: file }); const page = await context.newPage(); // 探测访问首页;被重定向到登录页说明缓存失效(跳转特征按你的系统调整) await page.goto(`${BASE_URL}/`); if (!/login/i.test(page.url())) { session = { page, context }; } else { await closeContexts(context); fs.rmSync(file, { force: true }); // 删除过期缓存,走下方 UI 登录分支重建 } } if (!session) { const context = await browser.newContext(); const page = await context.newPage(); await new LoginPage(page).loginAs(role); await saveLoginState(page, role); // 首次 UI 登录成功后刷新缓存 session = { page, context }; } return session; } /** 监听 /api/ 请求与响应,返回累积的 API 日志数组 */ export function setupApiLogging(page: Page): ApiEntry[] { const apiLog: ApiEntry[] = []; // 用 Request 对象关联请求与响应,避免同 URL 并发请求时状态码错配 const entryByRequest = new Map<Request, ApiEntry>(); page.on('request', req => { if (!req.url().includes('/api/')) return; const entry: ApiEntry = { method: req.method(), url: req.url() }; if (req.method() === 'POST') entry.body = req.postData() ?? undefined; apiLog.push(entry); entryByRequest.set(req, entry); }); page.on('response', resp => { const entry = entryByRequest.get(resp.request()); if (entry) { entry.status = resp.status(); entryByRequest.delete(resp.request()); } }); return apiLog; } /** 监听控制台错误与未捕获异常,返回错误信息数组 */ export function setupConsoleLogging(page: Page): string[] { const consoleErrors: string[] = []; page.on('console', msg => { if (msg.type() === 'error') consoleErrors.push(msg.text()); }); page.on('pageerror', err => consoleErrors.push(`Uncaught: ${err.message}`)); return consoleErrors; } /** 检测页面文本中是否出现服务端错误(5xx)。 * 避免裸匹配「500」造成误报(如「共 1500 条」「¥500」),只匹配带上下文的形式 */ export function isServerCrash(text: string): boolean { return ( /HTTP\s*5\d{2}/.test(text) || // HTTP 500 / HTTP 502 /\b5\d{2}\b\s*(?:Bad\s*Gateway|Internal\s*Server\s*Error)/i.test(text) || // 502 Bad Gateway(无 HTTP 前缀) /Internal\s*Server\s*Error|Bad\s*Gateway|服务器内部错误|网关错误/i.test(text) ); } export interface BugEvidence { id: string; description: string; screenshots: string[]; apiCalls: ApiEntry[]; consoleErrors: string[]; currentUrl: string; pageTitle: string; } /** 收集 Bug 证据:截图 + attach 到报告 + 记录 URL/标题/API/控制台。 * 截图命名与手动过程截图统一为小写 bug-{序号}-* 前缀(bugId 传 'BUG-001' 时存为 bug-001-汇总.png) */ export async function collectBugEvidence( page: Page, testInfo: TestInfo, bugId: string, description: string, options: { apiLog?: ApiEntry[]; consoleErrors?: string[]; fullPage?: boolean } = {}, ): Promise<BugEvidence> { const fullPage = options.fullPage ?? true; const screenshotPath = `${bugId.toLowerCase()}-汇总.png`; const buffer = await page.screenshot({ path: screenshotPath, fullPage }); await testInfo.attach(`${bugId}: ${description}`, { body: buffer, contentType: 'image/png' }); return { id: bugId, description, screenshots: [screenshotPath], apiCalls: options.apiLog ? [...options.apiLog] : [], consoleErrors: options.consoleErrors ? [...options.consoleErrors] : [], currentUrl: page.url(), pageTitle: await page.title(), }; } /** 一站式 Bug 跟踪:注册 API + 控制台监听,提供 collect() 一键收集证据 */ export function setupBugTracking(page: Page) { const apiLog = setupApiLogging(page); const consoleErrors = setupConsoleLogging(page); return { apiLog, consoleErrors, async collect( testInfo: TestInfo, bugId: string, description: string, options?: { fullPage?: boolean }, ) { return collectBugEvidence(page, testInfo, bugId, description, { apiLog: [...apiLog], consoleErrors: [...consoleErrors], ...options, }); }, }; } /** (进阶)用浏览器内 fetch 携带认证 token 调用 API:绕过 UI 直接造数据或校验后端 */ export async function apiFetch(page: Page, path: string, options?: RequestInit) { const token = await page.evaluate(() => { const raw = localStorage.getItem('<你的 token 存储 key>'); // 按实际调整 try { return raw ? (JSON.parse(raw).token ?? raw) : null; } catch { return null; } }); const headers: Record<string, string> = { 'Content-Type': 'application/json', ...((options?.headers as Record<string, string>) ?? {}), }; if (token) headers['Authorization'] = token.startsWith('Bearer ') ? token : `Bearer ${token}`; return page.evaluate( async ({ url, opts }) => { const resp = await fetch(url, opts); return { status: resp.status, body: await resp.text() }; }, { url: `${BASE_URL}${path}`, opts: { ...options, credentials: 'include' as RequestCredentials, headers } }, ); } ``` -
playwright-conventions.md 22.3 KB
# Playwright 工程约定与代码模板 > **路径口径**:本文档内 `../core/...`、`references/...` 等相对路径按消费方 SKILL.md 所在目录(`skills/automated-e2e-testing/`)解析书写,不是相对本文件。 > `automated-e2e-testing` 的脚手架、配置与场景代码模板全集。工作流零/一/二需要写代码时**加载本文件**;SKILL.md 只保留工作流与决策点。 ## 1. 项目脚手架 ``` playwright/ ├── playwright.config.ts # 配置 ├── package.json # 脚本入口 ├── tests/ │ ├── constants.ts # 常量(账号、URL、状态枚举) │ ├── helpers.ts # 通用函数(登录、多用户会话、Bug 证据收集) │ ├── pages/ # Page Objects │ │ └── xxx.page.ts # 每个页面一个文件 │ ├── {序号}-{模块}.spec.ts # 正式测试用例 │ └── explore-{功能}.spec.ts # 业务熟悉探索(临时) └── test-data/ # CSV 等测试数据 ``` 运行命令(依赖 package.json 的 scripts 定义,新项目需配置 `"test": "playwright test"`、`"test:headed": "playwright test --headed"`、`"show-report": "playwright show-report"`): ```bash cd playwright npm test # 全量运行 npm run test:headed # 有头模式(可视观察) npx playwright test tests/{文件名}.spec.ts # 运行单个文件 npm run show-report # 查看报告 ``` ## 2. 环境与账号配置 **不硬编码任何环境地址或账号。** 所有 URL、账号、角色映射集中配置在项目的 `tests/constants.ts`(或 `.env`)中,代码通过常量引用: ```typescript // tests/constants.ts export const BASE_URL = process.env.TEST_BASE_URL ?? '<你的测试环境地址>'; // 角色账号映射:key 为角色名,供 LoginPage.loginAs(role) 使用 export const ACCOUNTS = { admin: { username: '<管理员账号>', password: '<密码>' }, user: { username: '<普通用户账号>', password: '<密码>' }, // 按你的系统角色继续扩展 } as const; ``` > 账号、密码、真实用户 ID 等敏感信息不要提交到代码仓库,建议通过 `.env` + `dotenv` 注入,或在 CI 密钥中提供;`constants.ts` 只读取环境变量并提供默认占位。 ## 3. playwright.config.ts ```typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './tests', fullyParallel: false, // 默认串行(保守起点);用例独立性达标后按第 11 节渐进开启并行 timeout: 60_000, // 单个测试超时 60s retries: process.env.CI ? 1 : 0, // 本地零重试暴露问题;CI 至多重试 1 次,重跑变绿仍要按第 11 节定性 flaky use: { actionTimeout: 10_000, // 单个操作超时 10s screenshot: 'only-on-failure', trace: 'retain-on-failure', baseURL: process.env.TEST_BASE_URL ?? '<你的测试环境地址>', }, }); ``` > 注意:`actionTimeout` / `screenshot` / `trace` / `baseURL` 必须放在 `use` 内——写在配置顶层会被 Playwright 静默忽略,导致失败时无截图、无 trace。 ## 4. 探索测试模板(工作流零用) ```typescript // explore-{功能}.spec.ts — 临时文件,知识提取后删除 import { test } from '@playwright/test'; import { LoginPage, setupApiLogging } from './helpers'; test('业务熟悉:探索{功能名}页面结构', async ({ page }) => { const apiLog = setupApiLogging(page); await new LoginPage(page).loginAs('admin'); await page.goto('/<你的页面路由>'); await page.waitForLoadState('networkidle'); // 阶梯③:仅为让整页截图前网络静默,仅 SSR/MPA 页适用;SPA/长连接页改 waitForResponse(工程约定第 9 节) // 全页截图 await page.screenshot({ path: 'debug-{功能}-landing.png', fullPage: true }); // 枚举可交互元素 const buttons = await page.getByRole('button').allTextContents(); const tabs = await page.getByRole('tab').allTextContents(); console.log('按钮:', buttons); console.log('Tab:', tabs); console.log('当前 URL:', page.url()); // 按测试用例步骤操作,每步截图 // ... }); ``` ## 5. 场景代码模板(工作流一用) ### CRUD 操作 ```typescript test.describe('{模块名}', () => { let createdName: string; let xxxPage: XxxPage; // afterEach 清理要用,在 beforeEach 中初始化 test.beforeEach(async ({ page }) => { // 登录 + 进入页面 xxxPage = new XxxPage(page); }); test.afterEach(async () => { // beforeEach 可能登录失败导致 xxxPage 未初始化,同步判空防清理噪音 if (createdName && xxxPage) { await xxxPage.deleteXxx(createdName).catch(() => {}); } }); test('TC-01-01: {用例描述}', async () => { createdName = `{前缀}-${Date.now()}`; await xxxPage.createXxx(createdName); // getXxxByName 返回 Locator(见 §6 XxxPage)——web-first 断言自带轮询重试, // 不要先 await 再 not.toBeNull()(对 Locator 恒真,等于没断言) await expect(xxxPage.getXxxByName(createdName)).toBeVisible(); }); }); ``` ### 表单提交 ```typescript test('TC-01-02: {用例描述}', async ({ page }) => { // 1. 先注册响应等待(必须在操作之前;勿用固定 waitForTimeout,见 SKILL.md 速查表) const responsePromise = page.waitForResponse( resp => resp.url().includes('/api/target') && resp.request().method() === 'POST', ); // 2. 填充表单(fill 优先;受控组件校验不触发/值不生效时改用 keyboard.type,详见速查表) await page.locator('#field').fill('值'); // 3. 提交 await page.getByRole('button', { name: '提交' }).click(); // 4. 验证 API 调用 + 持久化 const response = await responsePromise; expect(response.ok()).toBeTruthy(); await page.reload(); await expect(page.getByText('值')).toBeVisible(); }); ``` ### 状态流转 ```typescript test('TC-01-03: {用例描述}', async ({ page }) => { // 前置:用对应 Page Object 创建一条可操作的业务数据 const { resourceName } = await createResourceWithData(page, 'TC-STATUS'); await xxxPage.performAction(); await expect(page.getByText('<目标状态文案>')).toBeVisible(); }); ``` ### 多用户并发 ```typescript test('TC-01-04: {用例描述}', async ({ browser }) => { // 创建两个独立会话(各自登录不同角色),见 references/helpers_reference.md const { pageA, pageB, contextA, contextB } = await createDualSession(browser, 'admin', 'user'); try { await pageA.getByRole('button', { name: '<动作A>' }).click(); await pageB.getByRole('button', { name: '<动作B>' }).click(); await expect(pageA.getByText('<预期文案>')).toBeVisible(); } finally { await closeContexts(contextA, contextB); } }); ``` ## 6. Page Object 编写规范 **复用优先**:检查 `tests/pages/` 下是否已有对应页面的 Page Object。 ```typescript // pages/xxx.page.ts import { Page, Locator, expect } from '@playwright/test'; import { BASE_URL } from '../constants'; export class XxxPage { readonly page: Page; constructor(page: Page) { this.page = page; } async goto() { await this.page.goto(`${BASE_URL}/xxx/list`); // 列表交互由首个断言/操作的 auto-wait 兜底(阶梯①);确需等列表接口时用 waitForResponse(②) } async createXxx(name: string) { // fill 优先;受控组件校验不触发/值不生效时改用 keyboard.type await this.page.getByRole('textbox', { name: /名称/ }).click(); await this.page.keyboard.type(name, { delay: 30 }); } // 行定位:返回 Locator 供 web-first 断言(toBeVisible 等)直接消费,勿 await 后判空 getXxxByName(name: string): Locator { const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // 唯一名含特殊字符时先转义 return this.page.getByRole('row', { name: new RegExp(escaped) }).first(); } async deleteXxx(name: string) { const row = this.getXxxByName(name); if (!await row.isVisible().catch(() => false)) return; // 行未渲染/已删除则跳过 await row.locator('[aria-label="delete"]').first().click(); await this.page.getByRole('button', { name: /确\s*定/ }).click(); } } ``` ## 7. 测试数据管理与 spec 命名 - **常量数据**:放 `constants.ts`(账号、URL、状态枚举) - **CSV 数据**:放 `test-data/` 目录 - **动态数据**:在测试中用 `Date.now()` 生成唯一名称;造数通道选择与 makeX 构造器模式见 `../core/methods/data-factory.md` | 类型 | 命名 | 示例 | |------|------|------| | 正式测试 | `{序号}-{模块}.spec.ts` | `01-resource-creation.spec.ts` | | 探索测试 | `explore-{功能}.spec.ts` | `explore-config.spec.ts`(**临时文件**) | ## 8. Bug 证据收集代码(工作流二用) ```typescript test('测试中发现 Bug', async ({ page }, testInfo) => { const tracker = setupBugTracking(page); await new LoginPage(page).loginAs('admin'); // 截图 1:操作前 await page.screenshot({ path: 'bug-001-01-操作前.png', fullPage: true }); // 触发异常的操作(先注册响应等待,再点击;勿用固定 waitForTimeout) const saveResponse = page.waitForResponse(resp => resp.url().includes('/api/save')); await page.getByRole('button', { name: '保存' }).click(); await saveResponse; // 截图 2:异常现象 await page.screenshot({ path: 'bug-001-02-异常.png', fullPage: true }); // 收集完整证据(截图 + API + 控制台) const evidence = await tracker.collect(testInfo, 'BUG-001', '保存失败无提示'); }); ``` ## 9. 等待策略与降级阶梯 Playwright 操作自带 auto-wait,`expect(locator)` 断言自带轮询重试。显式等待手段按下表**自上而下优先**选用: | 层级 | 手段 | 适用 | |------|------|------| | ① | auto-wait + `expect(locator).toBeVisible()` 等 | 绝大多数交互与断言 | | ② | `page.waitForResponse()` / `waitForURL()` 先注册再操作 | 提交/跳转类操作的确定性收口 | | ③ | `waitForLoadState('networkidle')` | 传统 SSR/MPA 页面的聚合加载收尾 | | ④ | 固定 `waitForTimeout` | 仅临时调试,禁止提交进正式 spec | **networkidle 失效场景必须降级**:页面存在 WebSocket/SSE 长连接、轮询心跳(监控上报、IM 未读数、灰度打点)时网络"永不空闲",等待会一直超时到 test timeout。降级方式:找到该操作真正触发的关键接口改用层级 ②;无确定接口时把验证交给层级 ① 的断言自动轮询。不确定是否存在长连接时,向用户提问而不是套 ④。 ## 10. 认证态复用(storageState) 大量用例都从登录页走完整 UI 登录会显著拉长执行时长。做法:首次 UI 登录成功后把上下文认证态落盘为 `.auth/{role}.json`;后续会话优先复用缓存——访问任一路径确认未被踢回登录页即视为有效;被踢回登录页(token 过期、登录流程变更)则删除缓存、回退 UI 登录并刷新缓存。 helper 参考实现(`saveLoginState` / `createSession`)见 `references/helpers_reference.md`。注意两点: - `.auth/` 目录含登录凭据,**必须加入 `.gitignore`** - 多用户并发场景(`createDualSession`)各角色各自走一遍缓存复用即可,互不影响 ## 11. flaky 治理与 CI 接入 ### flaky 二次确认规则 - **定性**:一条用例首次失败、原样重跑通过 → 判为 first-run-flaky,**不算稳定通过**;原样重跑只用于定性,不得成为常态化通过手段 - 定性后立即定位:回看 trace(第 3 节配置已开 `retain-on-failure`)、对比失败前后截图与 API 日志;常见根因是竞态等待不足(按第 9 节修正)或数据残留(清理不彻底) - 修复前的处置:短期标 `test.fixme` 防止污染主干绿灯,修复后恢复 - **禁止**靠调大 retries 或调大超时让用例"变绿" ### 并行开启条件 本 skill 核心原则要求每条 test 独立(自建数据 + 自清理)——独立性达标后就没有理由永久串行: - 开启前逐项核对:动态数据全部 `${前缀}-${Date.now()}` 唯一命名;afterEach 清理齐全;无跨用例共享可变状态;无互斥业务约束(如同一单据只能一人操作) - 渐进开启:先以 `npx playwright test tests/{模块目录} --workers=2` 小并发观察一轮,稳定后在配置中上调 `workers` - 有顺序依赖的遗留用例:用 `test.describe.configure({ mode: 'serial' })` 局部圈住,不阻塞其余并行 ### CI 接入要点 - **报告产物**:HTML 报告供 artifact 下载查看;需平台解析时另配 JUnit 输出: ```typescript // playwright.config.ts 增补(outputFile 对齐 core/pipeline-integration.md 约定二的 // 产物路径 {项目}/results/results.xml,供流水线 G 级信号聚合与机读摘要解析) reporter: [ ['html', { open: 'never' }], ['junit', { outputFile: '../results/results.xml' }], // 相对 playwright/ 工作目录 ], ``` - **触发策略**:合入前流水线只跑受影响模块目录;夜间任务跑全量 - **变量注入**:环境地址与账号沿用第 2 节约定,CI 上经变量/密钥注入,不入库 - 最小 GitHub Actions 片段: ```yaml # .github/workflows/e2e.yml 关键步骤 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npm ci && npx playwright install --with-deps chromium working-directory: playwright - run: npm test working-directory: playwright - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright/playwright-report/ ``` ## 12. 类型域执行层之一:无障碍扫描(轴 6 accessibility) 类型矩阵轴 6 决策 include 时按本节接入(决策与档位来自 test-strategy 的 type_scope,本节只管执行形态);报告回收走 report-template §7 表,执行方列如实填 axe-core。 一次性安装: ```bash npm i -D @axe-core/playwright ``` helper 封装(放 `tests/helpers/axe.ts`,一条命令接入的落地形态): ```typescript /** * runAxeScan —— 对当前页面执行 axe 可访问性扫描, * 只返回 critical / serious 阻断级违规的结构化清单,供分流表或 Cx 条目直接引用 */ import AxeBuilder from '@axe-core/playwright'; import type { Page } from '@playwright/test'; export async function runAxeScan(page: Page, label: string): Promise<string> { const results = await new AxeBuilder({ page }).analyze(); const blocking = results.violations.filter( v => v.impact === 'critical' || v.impact === 'serious' ); if (!blocking.length) return `${label} — 无阻断级违规`; const lines = blocking.flatMap(v => [ `[${v.impact}] ${v.id}: ${v.help}`, ...v.nodes.slice(0, 5).map(n => ` ${n.target.join(' ')}`), ]); return `${label} — ${blocking.length} 类阻断级违规:\n${lines.join('\n')}`; } ``` 处置纪律: - **分级判定**:critical / serious 计缺陷条目(critical 影响主流程可达性时 Severity 从 S1 起);moderate / minor 进观察清单不计 Bug——对比度阈值争议、读屏体验等主观项标人工复核(矩阵轴 6 边界条款) - light 档只扫 type_scope 给定的 P0 页面集,逐页出违规清单条目(Cx 通道);standard 档扩展到主要页面全集 + 关键流键盘走查(Tab 遍历、断言焦点态可见) - 扫描失败页面本身可渲染才可信——白屏页上的 axe 结果没有意义,先排除环境故障 ## 13. 类型域执行层之二:视觉基线(轴 7 visual) 配置增补(playwright.config.ts)——**只增补以下键,合并进既有 `defineConfig`**,勿整块替换(会丢掉 §3 的 timeout / retries / screenshot / trace 配置): ```typescript { // 截图基线独立目录:便于 CI 工件上传规则通配 snapshotPathTemplate: '{testDir}/__screenshots__/{testFileName}/{arg}{ext}', expect: { toHaveScreenshot: { maxDiffPixelRatio: 0.02 }, // 2% 容差起步,按页面校准收紧 }, } ``` 用例形态: ```typescript // TC-07-01: 订单列表页视觉基线比对(fullPage 整页基准) await expect(page).toHaveScreenshot('TC-07-01-full.png', { fullPage: true }); ``` 三条硬纪律(矩阵轴 7 防 flaky 条款同源): - **动态区必须声明 mask**——时间戳、头像、随机推荐位、广告轮播不遮必炸,这是纪律不是技巧: ```typescript await expect(page).toHaveScreenshot('dashboard.png', { fullPage: true, mask: [ page.locator('[data-testid="timestamp"]'), page.locator('.recommend-feed'), ], }); ``` - **未声明遮罩规则的 diff 失败不判 Bug**:先进 S 级复核区分动态内容干扰还是真实回归(走 `../core/triage.md` 个体定性路径后再下结论) - 基线更新只能 `npx playwright test --update-snapshots`,且**仅在分流判 B1(预期变更附依据)之后执行**——拿到红灯就重录基线等于把真回归洗白成绿灯;light 档不自动 diff,只截图存档供人比对 ## 14. 类型域执行层之三:多浏览器兼容(轴 5 compatibility) 配置增补(playwright.config.ts)——**在既有 `defineConfig` 中增补 `projects` 键**,勿整块替换;文件顶部 import 改为 `import { defineConfig, devices } from '@playwright/test'`(`devices` 由本节引入)。Playwright 原生引擎为 chromium/firefox/webkit,Edge 由 chromium channel 派生——恰好覆盖 light 档「最新 Chrome/Safari/Edge/Firefox 冒烟」定义: ```typescript projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, { name: 'msedge', use: { channel: 'msedge' } }, ], ``` 档位对接(depth 来自 type_scope,不为档位发明新语义): - light → 四 project × P0 冒烟子集:`npx playwright test tests/smoke/ --project=chromium --project=firefox --project=webkit --project=msedge` - standard → 需求支持清单对应的 project 子集 × P0 路径(不在支持清单内的浏览器不加 project) - full → 浏览器 × 分辨率矩阵逐格,分辨率经 `use: { viewport: {...} }` 维度展开 成本纪律:本地开发固定 `--project=chromium` 收敛反馈环;多浏览器留给流水线夜间档跑(非交互产物与退出码约定见 `../core/pipeline-integration.md`)——单浏览器本地 + 多浏览器 CI 是多浏览器成本的业界默认切分,全量永远并行只会让反馈环烂掉。 ## 15. 断言强度自检 > SKILL.md 工作流一第 3 步"断言三问"的完整版。原则:断言强度 = 变异杀伤能力—— > 把任一断言换成"恒真"后测试若仍通过,该断言不构成验证。实测教训(2026-08-29 变异门基线): > 搜索过滤逻辑与回车触发被删两个缺陷,On 臂三轮生成全部未杀——根因是前置只造了一条 > 匹配数据,单元素列表上"过滤"语义不可观察,任何断言都无区分度。 ### 三问完整版 1. **区分度(前置与反向断言)**:过滤 / 搜索 / 删除类操作,前置必须含**"不应匹配 / 应被排除"**的数据,断言必须覆盖"不在": ```typescript // 弱:前置只造一条匹配数据——过滤逻辑被删、搜索永不触发,toHaveCount(1) 照样全绿 await projectsPage.createProject(matching); await projectsPage.searchProject(matching); await expect(projectsPage.getProjectCards()).toHaveCount(1); // 强:前置造一匹配一不匹配,断言反向——过滤被删(列表恒为全集)立即失败 await projectsPage.createProject(matching); await projectsPage.createProject(other); // 不应出现在搜索结果里 await projectsPage.searchProject(matching); await expect(projectsPage.getProjectCardByName(matching)).toBeVisible(); await expect(projectsPage.getProjectCardByName(other)).toHaveCount(0); ``` 2. **触发方式保真**:被测行为步骤的触发方式是用例规格的一部分——用例写"回车触发搜索"就用 `keyboard.press('Enter')` 驱动;为绕开元素不稳定改用 input / blur / 直接调函数等替代路径 = 没测被测行为。不稳定走第 9 节等待降级阶梯;降不动按 SKILL.md 提问纪律问用户,不改触发路径。 **作用域限定**:仅约束被测行为步骤——前置准备是合法捷径(登录态 storageState 复用见 第 10 节、造数走 API),不受本条约束。 3. **环境区分度**:断言失败时能区分"功能坏了"和"环境/页面没加载"吗——服务端崩溃 / 白屏 先经 `isServerCrash` 与环境判定(pipeline-integration C 类),不带病定性。 **基线不稳的测试没有验证资格**:一条测试自身时好时坏时,先按第 11 节定性 flaky 修稳定性, 再谈它能不能发现缺陷。 ### UI 断言三件套(对齐 api-testing 断言三件套) | 件 | 断什么 | 手段 | |---|---|---| | 状态可见变化 | 操作后的 UI 反映 | `expect(locator)` web-first 断言 | | 持久化 | 数据真正落库 | `page.reload()` 后重断言 | | 关键内容 | 文案 / 数值正确 | 断言具体值,不止元素存在 | ### 弱断言黑名单(出现即改) - 只断言"无报错 / 页面没崩" - 只断言元素存在,不断言内容与状态 - try/catch 吞异常后测试照常通过 - 固定 `waitForTimeout` 后断言(掩盖时序,见第 9 节) - 断言实现细节(类名 / 内部状态)而非用户可感知行为 ## 16. 网络拦截边界(route mock) E2E 只 mock **不可控的外部依赖**(第三方支付 / 短信 / 风控 / 地图等)——被测系统自身的 API 一律打真实后端,不得 route mock 代替(mock 掉被测系统 = 测的是 mock 不是系统)。 关键集成按类型矩阵轴 10 standard 口径"mock 与真实双跑": ```typescript // 只拦外部域名;被测域名的请求全部放行 await page.route('**//api.pay.example.com/**', route => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, msg: 'mock 支付成功' }), })); ``` - mock 响应结构与错误码以被 mock 服务的公开契约为准(契约来源写进测试注释),不自造字段 - 真实联调环境可用时优先真实双跑:mock 版验证本方分支逻辑,真实版验证集成—— 只有 mock 一条腿不算集成通过 - mock 未命中(第三方改版)视为环境问题走 C 类分流,不是被测系统缺陷
-
-
SKILL.md 15.7 KB
--- name: automated-e2e-testing slug: automated-e2e-testing displayName: E2E 自动化测试 version: 0.8.1 description: "Turn manual cases into Playwright E2E automation run in a browser: page objects, helpers, bug evidence, reports. Not for: API tests, exploratory sessions, bug root-cause. 将手动用例转为 Playwright E2E 自动化并真实执行;含写自动化前的业务熟悉踩点、Page Object/Helper、Bug 证据与报告条目。不用于:API 接口测试、独立探索会话(exploratory-testing)、Bug 根因。" --- # 自动化 E2E 测试(automated-e2e-testing) 本 skill 覆盖 Web 应用自动化测试的完整工作流:将手动测试用例(markmap + Schema)转化为 Playwright spec → 运行并验证 → 发现 Bug → 输出测试报告。 核心原则: 1. **先熟悉业务再写测试** — 对功能不熟悉时,先用自动化脚本主动探索系统,理解实际行为后再动手写测试代码 2. **有疑问就提问,不自行假设** — 编写过程中遇到任何不明确的地方,必须向用户提问澄清,绝不凭猜测写代码 3. **每条自动化用例对应一条手动用例,每条 test 只测一个点** 4. **每个 test 独立**(自建数据 + 自清理) 5. **必须使用 Page Object** — 正式测试中禁止裸写定位器,所有页面交互封装在 Page Object 中 工程约定(脚手架、配置、场景代码模板、Page Object 规范)统一在 `references/playwright-conventions.md`——写代码时加载;通用 Helper(登录、多会话、证据收集)的参考实现在 `references/helpers_reference.md`;类型域三类执行片段(a11y 扫描 / 视觉基线 / 多浏览器矩阵)见工程约定第 12–14 节,type_scope 判入对应轴时按档加载取用 ## When to Use - 给定测试用例(markmap / Schema),需要生成 Playwright spec 文件并执行 - 编写自动化前,需要小规模业务熟悉探索(踩点页面结构、提取选择器) - 自动化执行中发现 Bug,需要收集证据并记录测试报告条目 - 需要编写新的 Page Object 或 Helper 函数 ## When NOT to Use - 端到端测试整个需求(理解→策略→用例→执行→报告的流水线)→ 用 `qa` skill 编排 - 编写手动测试用例 → 用 `test-case-writing` skill - 纯 API 接口测试(无 Web UI 流程)→ 用 `api-testing` skill - 以**理解系统 / 发现风险**为目的的独立探索式测试会话(charter 驱动、产出探索笔记)→ 用 `exploratory-testing` skill;本 skill 的工作流零只做「为写自动化踩点」的小规模探索 - 已确认 Bug 的根因定位、影响分析、回归建议 → 用 `bug-analysis` skill;本 skill 只负责收集 Bug 证据(截图/API/控制台)并记录报告条目 - 代码变更后判断回归范围 → 用 `regression-testing` skill - 单元测试 → 用 Jest/Vitest;性能压测 → 专业工具(k6、locust);安全测试 → 安全审计专项(见 `test-strategy` 的 handoff 约定) ## 提问时机(必须遵守) **核心规则:不确定就问,宁可多问不要瞎猜。** 格式与裁决规则统一按 `../core/clarify-pattern.md`(场景用「执行确认」)。 | 场景 | 应提问的内容 | 不要自行假设 | |------|-------------|-------------| | 元素定位失败 | "在{页面}上找不到{元素},实际页面结构是否与预期一致?" | 不要随意换选择器猜测 | | 操作路径不明确 | "测试用例说{操作X},但页面上没有直接的入口" | 不要自行拼凑操作步骤 | | 预期行为有歧义 | "预期{结果A},实际{结果B},应以哪个为准?" | 不要选择性地相信其中一个 | | 业务规则不清楚 | "规则{X}的具体边界是什么?" | 不要用常见默认值代替 | | 探索中发现异常 | "发现{异常行为},这是预期行为还是 Bug?" | 不要自行判定是 Bug 还是特性 | | 用例反复超时/不稳定 | "{页面}是否存在长连接或轮询推送(WebSocket/SSE/心跳上报)导致页面永不空闲?" | 不要一律套 networkidle 等待,按等待降级阶梯处理 | ## 工作流零:业务熟悉(前置必做,为写自动化踩点的小规模探索) **何时需要**:从未测试过该功能模块 / 出现不熟悉的页面路由 / 需要编写新的 Page Object / 拿到用例但不知道系统长什么样 > 本工作流是**小规模踩点探索**(理解页面结构、提取选择器、落 Page Object),产出服务于工作流一。以理解系统 / 发现风险为目的的**完整探索会话**(charter 驱动、产出探索笔记)用 `exploratory-testing` skill。 ### 步骤 1. **探索页面结构**:登录 → 导航到目标页面 → 截图 → 枚举所有可交互元素 2. **体验核心流程**:按测试用例步骤走一遍完整流程,每步截图,记录 API 调用 3. **记录发现**:页面导航路径、关键元素选择器、API 接口、隐藏行为、**编写或更新 Page Object** 探索代码模板见 `references/playwright-conventions.md` 第 4 节(explore-*.spec.ts)。 ### 完成标准 - [ ] 每个涉及的页面都有截图 - [ ] 理解了页面间的导航路径 - [ ] 确认了关键元素的选择器 - [ ] 记录了实际的 API 请求 - [ ] 发现了隐藏行为(隐藏字段、默认值、前置条件) - [ ] 已编写或更新了对应的 Page Object --- ## 工作流一:手动用例 → 自动化代码 > **前提**:已完成工作流零(业务熟悉),对目标功能有充分认知。 ### 用例输入与自动化范围判定 输入是 `test-case-writing` 产出的 markmap 用例文件(如含 `测试用例.schema.yaml` 则一并消费): - Schema 的 `execution_model: ui` 且 `automation.supported != no` 的用例 → 本工作流的转换对象 - `execution_model: dev-collab`(无 UI 协作用例)→ 移交 `api-testing` 或保持手动协作执行,不硬造 UI 自动化 - `automation_plan` 存在时(`test-strategy` 产出)按用户的执行策略裁决执行;策略未定时向用户确认哪些用例转自动化 ### 步骤 1. **解析测试用例**:从 markmap Markdown 提取场景与预期;test 名沿用用例的 TC 编号(如 `TC-01-03: {用例名称}`),手动用例缺编号时先补编号再转换 2. **选择/新建 Page Object**:检查 `tests/pages/` 下是否已有对应 Page Object,优先复用 3. **编写 spec**:使用 Page Object 封装所有页面交互。每条 test 收笔前过**断言三问**:① 前置里有"不应匹配/应被排除"的数据吗?断言覆盖了"不在"吗?② 被测步骤的触发方式与用例规格一致吗(不得为绕开不稳定换触发路径)?③ 失败时能区分"功能坏了"还是"环境没加载"吗?——判定与反例见工程约定第 15 节 4. **运行验证**:`npx playwright test tests/{文件名}.spec.ts`;**基线零通过禁交付**——首轮运行 0 条通过即视为流程认知错误(选择器/弹窗结构/等待假设与真实页面不符):回工作流零针对性重踩核实,修订复跑至 ≥1 条通过后才可进入后续步骤,禁止交付基线零通过的规格 脚手架、场景代码模板(CRUD / 表单提交 / 状态流转 / 多用户并发)、Page Object 规范与 spec 命名**此时加载 `references/playwright-conventions.md`**(第 1、5–7 节)。 --- ## 工作流二:Bug 探索与记录 在已理解业务逻辑的前提下,系统性验证页面功能,发现 Bug 和不一致。 > 批量失败前置分流:一轮执行结束**失败 ≥3 条**时,先加载 `../core/triage.md` 做四分类定类(A 真缺陷 / B 资产问题 / C 环境 / D 不稳定),仅 A 类进入本工作流的证据收集与报告条目;<3 条维持单条流程不变。 > > headless 流水线场景(PR 冒烟 / 夜间全量 / 发布卡点):检查点降级为未决项、产物落盘规范与退出码语义按 `../core/pipeline-integration.md`(此时加载);其回流闭环中的批量失败同样进入上方分流流程。 > 职责边界:本工作流负责**发现 + 证据收集 + 报告条目**。Bug 被**确认**后的根因定位、影响分析、回归建议移交 `bug-analysis` skill(对应报告条目中根因分析等五个扩展字段留 TODO)。 ### Bug 发现策略 | 策略 | 检查方式 | 典型 Bug | |------|---------|---------| | UI 预期不一致 | 对比测试用例与实际截图 | 默认值不对、文案错误 | | 表单校验缺失 | 提交非法数据 | 必填不校验、范围不限制 | | 网络异常 | 监听 API 响应 | 500 错误、超时 | | 数据不一致 | 操作后 reload 验证 | 提交成功但数据丢失 | | 状态流转错误 | 多步操作验证状态 | 状态未更新、卡死 | | 控制台错误 | `setupConsoleLogging(page)` 持续监听 | 组件崩溃、静默失败 | ### Bug 证据收集(发现 Bug 时必须执行) ``` Bug 发现 → 截图操作前 → 操作触发异常 → 截图操作后 → 收集 API/控制台日志 → 写入报告 ``` 证据收集代码(`setupBugTracking` / `tracker.collect`)见 `references/playwright-conventions.md` 第 8 节;通用 Helper 落地时**加载 `references/helpers_reference.md`**。 截图命名统一小写 `bug-{序号}-*` 前缀:手动截图记录过程两态(`bug-001-01-操作前.png`、`bug-001-02-异常.png`);`tracker.collect()` 自动补一张整页汇总截图(`bug-001-汇总.png`)并 attach 到 HTML 报告。Bug 报告标题中的 `BUG-{序号}` 仅为显示格式。 ### Bug 报告格式 记录在 `{项目名}/测试报告_{来源}_{日期}.md`,**条目字段与 `../core/report-template.md` §3(测试报告唯一来源模板,此时加载)保持一致**,保证条目可直接拼装进 `qa` 收尾的最终报告;本 skill 的发现方式含「业务熟悉探索」。执行分报告还需包含 §2 执行统计(P0/P1/P2 × 用例数/通过/失败/阻塞/未执行表,同样按 report-template);条目中根因分析等五个扩展字段留 TODO,由 `bug-analysis` 填写。按 report-template「机读摘要」约定,分报告末节追加机读摘要片段(占位符替换后),供 `qa` 收尾与后续回归编排机读解析。 --- ## Explore 文件生命周期管理 业务熟悉探索文件(explore-xxx.spec.ts)是**临时产物**,遵循以下生命周期: ``` 创建 explore-xxx.spec.ts → 提取知识到 Page Object → 删除 explore 文件 + 截图 ``` ### 规则 1. **知识提取后立即删除**:将发现的选择器、API 路径、页面结构写入 Page Object 或 helpers 后,删除 explore 文件 2. **截图清理**:探索产生的 `debug-*.png` 截图是临时调试产物,确认不需要后删除 3. **不积累**:`tests/` 目录下不应存在已完成的 explore 文件。如果存在,说明知识提取步骤被跳过了 4. **Bug 报告截图保留**:`bug-{序号}-*.png` 截图属于 Bug 报告的一部分,不删除 --- ## 关键技巧速查表 > 下表方法名(`createDualSession` / `tracker.collect` / `setupBugTracking` / `isServerCrash` 等)为本 skill **脚手架内置 helper 的示例**(定义在 `references/`),不是 Playwright 通用 API——换项目时按 `references/helpers_reference.md` 适配实际脚手架,勿假定这些函数存在。 | 场景 | 方法 | 备注 | |------|------|------| | 受控组件输入框(Ant Design 等) | `fill()` 优先,不生效再 `keyboard.type()` | `fill` 会派发 input 事件,多数受控组件可用;`keyboard.type` 逐键最稳但慢 | | 确认弹窗按钮 | 在 `role=dialog` 作用域内定位(如 `page.getByRole('dialog').getByRole('button', { name: '确定' })`) | 防止与页面同名按钮歧义命中;弹窗外「确定/删除」同名按钮是常见误击源 | | 隐藏必填字段 | `page.evaluate()` 直接设值 | 阻止表单提交的常见原因 | | 网络请求验证 | `page.on('request')` 监听 | 必须在操作之前设置 | | 数据持久化验证 | `page.reload()` + 断言 | 确认数据真正保存 | | 多用户会话 | `createDualSession(browser, roleA, roleB)` | 两个角色并发 | | 登录态复用 | `createSession(browser, role)` | 缓存 storageState 免重复 UI 登录(工程约定第 10 节) | | 等待策略 | ① auto-wait + `expect` 断言 → ② `waitForResponse` → ③ `networkidle`(仅传统 SSR/MPA) | 自上而下优先(工程约定第 9 节);固定 `waitForTimeout` 仅临时调试;长轮询/心跳页对 networkidle 永不空闲 | | 并行执行 | 默认串行,独立性达标后开 `workers` | 前提:自建数据 + 自清理 + 唯一命名逐项核对(工程约定第 11 节) | | flaky 定性 | 首次失败原样重跑通过 → 判 flaky | 重跑只用于定性;定位根因前标 `test.fixme`(工程约定第 11 节) | | 截图调试 | `page.screenshot({ fullPage: true })` | 每个关键步骤都截图 | | 服务端崩溃检测 | `isServerCrash(bodyText)` | 检测 500/502 错误 | | 唯一命名 | `${前缀}-${Date.now()}` | 避免测试间名称冲突 | | Bug 证据收集 | `tracker.collect(testInfo, id, desc)` | 一键收集截图+API+控制台 | | Bug 探索前置 | `setupBugTracking(page)` | 注册 API 和控制台监听 | ## Common Mistakes | 错误 | 后果 | 正确做法 | |------|------|---------| | 受控组件值不生效仍反复 `fill()` | 测试卡死或断言失败 | `fill` 失效时改用 `keyboard.type()`(见速查表) | | 网络监听放在 click 之后 | 捕捉不到请求 | 先注册 listener,再执行操作 | | 不清理测试数据 | 后续测试受影响 | afterEach 中删除创建的资源 | | 用固定 timeout 等待 | 时序不稳定 | 按等待降级阶梯换用:① auto-wait+expect 断言 → ② `waitForResponse` → ③ `networkidle`(工程约定第 9 节) | | 不验证持久化 | 数据可能只存在内存 | reload 后重新断言 | | 正式测试不使用 Page Object | 代码重复,难以维护 | 所有页面交互封装在 Page Object 中 | | explore 文件不删除 | 上下文污染,AI 每次读大量废代码 | 知识提取后立即删除 | | 遇到疑问自行假设 | 产出不可靠的测试 | 不确定就向用户提问 | | Bug 只截图不记录 API/控制台 | 开发无法定位根因 | 用 `setupBugTracking()` + `tracker.collect()` | | 硬编码环境地址/账号 | 无法跨环境运行、泄露敏感信息 | 统一放 `constants.ts` / `.env`,代码只引用常量 | | 失败重跑变绿就当没事 | flaky 混入主干,CI 随机红 | 按 flaky 定性流程处理:先定性再修根因,禁止调大重试硬压(工程约定第 11 节) | | 对长轮询/推送页强套 networkidle | 超时假失败 | 按等待降级阶梯换 `waitForResponse`/断言自动轮询(工程约定第 9 节) | | 每个 context 都走一遍登录页 | 执行时长翻倍、缓存认证态形同虚设 | 用 `createSession()` 复用 storageState,失效自动回退 UI 登录(工程约定第 10 节) | | 所有用例永久串行不敢并行 | 全量执行时长线性膨胀 | 独立性核对通过后渐进开启 `workers`(工程约定第 11 节) | | 手动用例信息不足仍硬造定位器与操作路径 | 幻觉自动化:断言全绿但没测到真实行为 | 先过 `../core/executability.md` 转换闸门 + 工作流零踩点核实页面结构 | --- ## 与其他 skill 配合 - **上游**:`test-case-writing` 产出手动用例(markmap + Schema)→ 本 skill 把 `execution_model: ui` 且可自动化的用例变成可执行代码;端到端流水线由 `qa` 编排 - **旁路**:无文档 / 系统陌生的**完整探索会话**用 `exploratory-testing`,其探索笔记可作为本 skill 踩点的输入 - **下游**:Bug 确认后的根因 / 影响 / 回归分析 → `bug-analysis`;执行报告与 Bug 条目按 `../core/report-template.md` 对齐,供 `qa` 收尾汇总 - **平级**:纯 API 用例的自动化 → `api-testing`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.