Claude Skill

automated-e2e-testing

将手动测试用例转为 Playwright E2E 测试并执行时使用;含写自动化前的业务熟悉踩点、Page Object/Helper 编写、执行中的 Bug 证据收集与报告条目记录。不用于:纯 API 接口测试(api-testing)、以理解系统为目的的独立探索会话(exploratory-testing)、已确认 Bug 的根因分析(bug-analysis)。

LLM Mart · 0 points · 6 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download fishzjp-qa-skills-skills_automated-e2e-testing-9d93d04.zip · 22 KB
Part of fishzjp/qa-skills — 11 skills

Install

skills CLI npx skills add https://github.com/fishzjp/qa-skills/tree/main/skills/automated-e2e-testing
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install fishzjp-qa-skills@llmmart
Git 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 → 输出测试报告。

核心原则:

  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},应以哪个为准?" 不要选择性地相信其中一个
业务规则不清楚 "规则的具体边界是什么?" 不要用常见默认值代替
探索中发现异常 "发现{异常行为},这是预期行为还是 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
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.

No comments yet.

Reviews (0)

No reviews yet.

Related