mcpField
C
create_e2e_tests

create_e2e_tests

github.com/shiplightai测试与质量275 次安装18 周前更新
代码仓库

关于 create_e2e_tests

create_e2e_tests 是用于创建本地 YAML 端到端测试的分阶段技能,面向需要先规划再编写 Web 应用测试的开发者。它通过策略、场景规格和实施计划等文档衔接测试编写与覆盖核验,并使用 Shiplight 在本地运行测试。

主要功能

  1. 01按阶段生成测试规划文档
  2. 02用 Given/When/Then 描述场景
  3. 03通过浏览器采集真实定位器
  4. 04校验 YAML 测试文件
  5. 05对照规格核验测试覆盖

适用场景

  1. 01从零创建本地测试项目
  2. 02为 Web 应用添加 YAML 测试
  3. 03规划登录与关键用户流程测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/shiplightai/agent-skills --skill create_e2e_tests

安装后在 Claude Code 中输入/create_e2e_tests确认已加载。

详细介绍

创建本地 YAML 测试

一种由规范驱动的工作流,通过在编写任何测试之前进行结构化规划,提前引入测试专业知识。使用 npx shiplight test --headed 运行测试——无需云基础设施。

适用场景

当用户希望进行以下操作时,使用 /create_e2e_tests:

  • 从头创建新的本地测试项目
  • 为 Web 应用添加 YAML 测试
  • 为测试项目设置身份验证
  • 在编写测试前规划测试内容

原则

  1. 始终生成产物。 每个阶段都会写入一个 Markdown 文件。产物有助于厘清你的思路,为用户提供可供审阅的内容,并指导后续阶段。用户提供了详细需求时,将其用作素材——跳过已回答的问题,但仍要生成产物。

  2. 实现前先确认。 在投入时间进行浏览器探索和编写测试前,先呈现规范(第 2 阶段的检查点)供用户确认。将你的理解整理成结构化场景复述给用户,以便及早发现不一致之处。

  3. 每个阶段都读取上一阶段的产物。 Discover 为 Specify 提供依据,Specify 为 Plan 提供依据,Plan 为 Implement 提供依据,Implement 为 Verify 提供依据。如果之前运行时已生成产物,可询问是否复用。

  4. 遇到问题时升级处理,不要循环重试。 出现失败或含糊不清的情况时,向用户说明并询问,而不是悄悄重试。

阶段概览

Phase 1: Discover  → test-strategy.md    (understand the app & user goals)
Phase 2: Specify   → test-spec.md        (define what to test in Given/When/Then)
Phase 3: Plan      → test-plan.md        (prioritize, structure, per-test guidance)
Phase 4: Implement → *.test.yaml files   (setup project, write tests, run them)
Phase 5: Verify    → updated spec files  (coverage check, reconcile spec ↔ tests)

快速通道

开始前检查是否已有产物。只有用户明确提出时,才可以跳过产物生成。

情况处理方式
用户明确说“跳到实现”或“直接编写测试”仅执行第 4 阶段
已存在 test-specs/test-strategy.md询问是否复用,并跳过第 1 阶段
已存在 test-specs/test-spec.md询问是否复用,并跳过第 1–2 阶段
已存在 test-specs/test-plan.md询问是否复用,并跳到第 4 阶段

第 1 阶段:探索

目标: 了解应用、用户的角色,以及测试中最重要的内容。

输出: <project>/test-specs/test-strategy.md

步骤
  1. 获取项目路径 — 询问要在哪里创建测试项目(例如 ./my-tests)。所有产物和测试都会放在这里。创建 test-specs/ 目录。

    如果云 MCP 工具可用(已设置 SHIPLIGHT_API_TOKEN),使用 /cloud 技能获取环境和测试账号——这有助于预先填写目标 URL 和凭据。

  2. 静默扫描 — 提问前,先从现有信息中收集上下文:

    • 代码库:路由、组件、package.json、框架
    • Git 分支差异(近期有哪些变更)
    • 现有测试(已覆盖的内容)
    • PRD、文档、README 文件
    • 云环境(如果云 MCP 工具可用)
  3. 了解测试需求 — 询问用户想测试什么,然后一次提出一个有针对性的后续问题,并根据扫描结果给出建议,以补齐遗漏:风险区域、用户角色、身份验证、数据策略、关键用户旅程。跳过用户已经回答的问题。

  4. 编写 test-strategy.md,包含:

    • 应用概况:名称、URL、框架、关键页面/功能
    • 风险概况:最重要的内容、脆弱之处
    • 测试范围:包含/不包含的内容、要覆盖的用户角色
    • 数据策略:如何创建和清理测试数据
    • 环境:目标 URL、身份验证方式、任何特殊设置

第 2 阶段:编写规范

目标: 以结构化的 Given/When/Then 格式定义具体测试场景,并按风险排序。找出可能导致测试不稳定或不完整的模糊之处。

输入: 读取 test-specs/test-strategy.md

输出: <project>/test-specs/test-spec.md

步骤
  1. 阅读 test-strategy.md,了解范围和优先级。

  2. 生成用户旅程规范 —— 为每个关键旅程编写:

    • 标题:描述性名称(例如,“新用户通过电子邮件验证注册”)
    • 优先级:P0(必需)、P1(应该具备)、P2(锦上添花)
    • 前置条件:测试开始前必须满足的条件(Given)
    • 顺利流程:逐步列出操作和预期结果(When/Then)
    • 边缘情况:每个旅程至少 2 种(例如,无效输入、超时、空状态)
    • 数据要求:所需的测试数据
  3. 检查测试风险 —— 检查每个旅程中可能导致测试不稳定或不完整的问题:数据依赖、计时/异步行为、动态内容、身份验证边界、第三方服务、状态隔离、环境差异。在每个旅程中添加 测试注意事项 部分,列出已识别的风险和缓解措施。如果存在任何不明确之处,请一次询问用户一个问题,并提供建议答案和影响说明。

  4. 编写包含所有旅程规范的 test-spec.md。

  5. 检查点 —— 提交汇总表供用户审核:

    #旅程优先级步骤数边缘情况风险
    1用户注册P053计时
    2...............

    询问:“这样看起来正确吗?需要添加、删除或调整优先级的旅程吗?”

    等待用户确认后再继续。


第 3 阶段:规划

目标: 制定可执行的实施计划,并为每项测试提供指导。

**输入:**读取 test-specs/test-spec.md

输出:<project>/test-specs/test-plan.md

步骤
  1. 阅读 test-spec.md。

  2. 定义测试文件结构 —— 将旅程映射到测试文件:

    tests/
    ├── auth.setup.ts          (if auth needed)
    ├── signup.test.yaml        (Journey 1)
    ├── checkout.test.yaml      (Journey 2)
    └── ...
    
  3. 确定实施顺序 —— 按以下顺序排列:

    • 优先处理依赖项(先设置身份验证,再进行需要身份验证的测试)
    • 然后按优先级排序(P0 优先于 P1)
    • 最后按风险排序(风险最高的优先)
  4. 逐项测试指导 —— 为每个测试文件指定:

    • 数据策略:要创建/使用的数据、清理方法
    • 等待策略:何处使用 WAIT_UNTIL 与 WAIT、预期的加载节点
    • 不稳定性风险:此测试中需要注意的具体事项
  5. 编写 test-plan.md。

  6. 检查点 —— 提交汇总:

    已准备好实施 N 个测试文件。要继续吗?


第 4 阶段:实施

目标: 设置项目并根据计划编写所有 YAML 测试。

**输入:**读取 test-specs/test-plan.md

设置

跳过已完成的步骤(项目已存在、依赖项已安装、身份验证已配置)。

  1. 配置 AI 提供商 — 检查测试项目是否已有包含 AI API 密钥的 .env。如果没有,请询问用户选择哪个提供商:

    要运行 YAML 测试,我需要一个 AI 提供商来解析测试步骤。您想使用哪个提供商?

    A) Google AI — GOOGLE_API_KEY(获取密钥)— 默认模型:gemini-3.1-flash-lite-preview B) Anthropic — ANTHROPIC_API_KEY(获取密钥)— 默认模型:claude-haiku-4-5 C) OpenAI — OPENAI_API_KEY(获取密钥)— 默认模型:gpt-5.4-mini D) Azure OpenAI — 需要 AZURE_OPENAI_API_KEY + AZURE_OPENAI_ENDPOINT — 设置 WEB_AGENT_MODEL=azure:<deployment> E) AWS Bedrock — 使用 AWS 凭证链 — 设置 WEB_AGENT_MODEL=bedrock:<model_id> F) Google Vertex AI — 使用 GCP 应用默认凭证 — 设置 WEB_AGENT_MODEL=vertex:<model> G) 我已经配置好了

    用户选择后,请索取其 API 密钥并将其保存到测试项目的 .env 文件中。对于 A/B/C,会根据密钥自动检测模型。对于 D/E/F,还需使用相应的 provider:model 前缀保存 WEB_AGENT_MODEL。用户也可以选择设置 WEB_AGENT_MODEL 来覆盖默认模型(例如,WEB_AGENT_MODEL=claude-sonnet-4-6)。

  2. 搭建项目框架 — 使用绝对项目路径调用 scaffold_project。这会创建 package.json、playwright.config.ts、.env.example、.gitignore 和 tests/。将 API 密钥保存到 .env。

  3. 安装依赖项:

    npm install
    npx playwright install chromium
    
  4. 设置身份验证(如有需要) — 遵循标准的 Playwright 身份验证模式。

    在 playwright.config.ts 中将凭证添加为变量:

    {
      name: 'my-app',
      testDir: './tests/my-app',
      dependencies: ['my-app-setup'],
      use: {
        baseURL: 'https://app.example.com',
        storageState: 'tests/my-app/.auth/storage-state.json',
        variables: {
          username: process.env.MY_APP_EMAIL,
          password: { value: process.env.MY_APP_PASSWORD, sensitive: true },
          // otp_secret_key: { value: process.env.MY_APP_TOTP_SECRET, sensitive: true },
        },
      },
    },
    

    标准变量名:username、password、otp_secret_key。对于机密信息,使用 { value, sensitive: true }。将值添加到 .env。

    使用标准的 Playwright 登录代码编写 auth.setup.ts。对于 TOTP,使用 node:crypto 实现 RFC 6238(HMAC-SHA1 + base32 解码)——无需第三方依赖。

    继续之前先验证身份验证。 运行 npx shiplight test --headed 执行身份验证设置,并确认其保存了 storage-state.json。如果失败,请升级给用户处理——身份验证是其他所有操作的前提。

    如果测试计划涉及特殊身份验证要求(例如,每个测试使用一个账户、多个角色),请在继续之前与用户确认身份验证策略。

编写测试

对于计划中的每项测试(或用户需要的每项测试):

  1. 打开浏览器会话 — 使用应用的 starting_url 调用 new_session。
  2. 逐步体验流程 — 使用 inspect_page 查看页面,然后使用 act 执行每项操作。这样可以从响应中捕获定位器。
  3. 捕获定位器 — 需要更多元素信息时,使用 get_locators。
  4. 构建 YAML — 按照下方的最佳实践编写 .test.yaml 内容。
  5. 保存并验证 — 写入 .test.yaml 文件,然后使用文件路径调用 validate_yaml_test,检查定位器覆盖率(至少需要 50%)。
  6. 关闭会话 — 完成后调用 close_session。

重要: 请勿凭空编写 YAML 测试。务必先在浏览器会话中体验应用,以捕获真实的定位器。没有定位器的测试会被 validate_yaml_test 拒绝。

使用 test-plan.md 指导时:

  • 在加载节点应用指定的等待策略
  • 覆盖规范中定义的边界情况和断言
运行测试

编写完所有测试后,运行测试:

npx shiplight test --headed

测试失败时:

  1. 报告 — 用一句话告诉用户哪个测试失败以及失败原因。
  2. 分类失败:
    • 实现修复(定位器错误、缺少等待、时序问题)→ 修复并重试。
    • 规范不匹配(应用行为与规范不同)→ 询问用户是更新规范还是跳过该场景。
  3. 升级处理修复无效的情况——不要反复尝试相同的方法。

阶段 5:验证

目标: 根据规范验证测试覆盖情况,并协调任何偏差。

**输入:**读取 test-specs/test-spec.md、test-specs/test-plan.md 和所有 .test.yaml 文件

仅当规范文档存在时才运行此阶段。

覆盖检查

对于规范中的每个用户旅程,确认测试覆盖了正常路径和列出的所有边缘情况。

提供覆盖情况摘要:

规范中的用户旅程优先级规定的场景编写的测试覆盖情况
用户注册P044✓
结账P032✗ — 未覆盖边缘情况“购物车为空”

标记缺口和额外内容(规范中未列出的测试步骤)。

协调

更新规范文档,使其与实际实现保持一致:

  1. 更新 test-spec.md — 标记跳过的场景并说明原因,添加实现过程中出现的场景,更新边缘情况以反映实际测试内容
  2. 更新 test-plan.md — 修正文件结构,记录与原计划的偏差
  3. 显示差异摘要 — 告诉用户变更了什么以及原因

这样可确保文档准确,便于今后维护和扩展测试。


YAML 格式参考

阅读 MCP 资源 shiplight://yaml-test-spec-v1.3.0,了解完整的语言规范(语句类型、模板、变量、测试套件、钩子、参数化测试)。

阅读 MCP 资源 shiplight://schemas/action-entity,了解所有可用操作及其参数。

YAML 编写最佳实践

这些最佳实践结合 YAML 语言规范和操作目录,帮助你编写快速、可靠的测试。

语句类型选择
  • ACTION 是默认选项。 在浏览器会话期间,通过 MCP 工具(act、get_locators)捕获定位器,然后编写 ACTION 语句。ACTION 可确定性地重放(约 1 秒)。
  • DRAFT 是最后的选择。 仅当编写时确实无法确定定位器时才使用 DRAFT。DRAFT 速度较慢(每个约 5–10 秒,运行时由 AI 解析)。过多使用 DRAFT 的测试会被 validate_yaml_test 拒绝。
  • 使用 VERIFY 编写断言。 所有断言都使用 VERIFY:。不要编写类似 "Check that the button is visible" 的断言 DRAFT。
  • 使用 URL 进行导航。 使用 URL: /path 导航,而不是 action: go_to_url。
  • 使用 CODE 编写脚本。 使用 CODE: 进行网络模拟、localStorage 操作和页面级脚本编写。不要用它来执行点击、断言或导航。
intent 字段

intent 是步骤的意图——它定义了该步骤应完成_什么_。action/locator 或 js 字段是实现_方式_的缓存。缓存失效时(定位器过期、DOM 发生变化),AI Agent 会利用 intent 重新检查页面,并从头重新生成操作。

由于 intent 用于驱动自我修复,因此必须具体到足以让 Agent 在没有其他上下文的情况下执行。描述用户目标,而非 DOM 元素——避免使用元素索引、CSS 选择器或位置引用,因为这些内容会在 UI 变化时失效:

# BAD: vague, agent can't re-derive the action
- intent: Click button

# BAD: tied to DOM structure that can change
- intent: Click the 3rd button in the form
- intent: Click element at index 42

# GOOD: describes the user goal, stable across UI changes
- intent: Click the Submit button to save the new project
  action: click
  locator: "getByRole('button', { name: 'Submit' })"
ACTION:结构化格式与 js: 简写

默认情况下,所有受支持的操作都使用结构化格式。 阅读 MCP 资源 shiplight://schemas/action-entity,了解所有可用操作及其参数。

仅当操作无法映射到受支持的操作时才使用 js:——例如,复杂的多步交互、自定义 Playwright API 调用或链式操作:

- intent: Drag slider to 50% position
  js: "await page.getByRole('slider').first().fill('50')"

- intent: Wait for network idle after form submit
  js: "await page.waitForLoadState('networkidle')"
js: 编码规则
  • 始终将定位器解析为单个元素(例如,.first()、.nth(1)),以避免 Playwright 严格模式错误
  • 执行操作时始终指定 { timeout: 5000 },以确保时序可预测
  • intent 至关重要——当 js 失败时,它是自我修复的输入
  • page、agent 和 expect 在当前作用域中可用
VERIFY 最佳实践
  • 对带有 AI 回退的 js: 断言,始终设置较短的超时时间(例如,{ timeout: 2000 }),这样失效的定位器会快速回退到 AI,而不是等待默认的 5 秒
  • 始终使用 VERIFY: 简写——不要直接使用 action: verify
  • 注意 js: 断言可能出现误报阴性。 只有当 js 抛出错误(未找到元素、超时)时,才会触发 AI 回退。如果 js 针对错误元素执行成功(失效的选择器匹配了其他元素),断言会静默通过——不会触发回退。请保持 js: 断言简单且具体,以尽量降低此类风险。
IF/WHILE js: 条件最佳实践
  • 对基于 DOM 的检查(元素是否可见、文本是否存在、页面状态)使用自然语言(AI)条件。AI 条件可以根据 DOM 变化自我修复;js: 条件则很脆弱,无法自动修复。
  • js: 条件仅用于计数器/状态逻辑——例如,js: counter++ < 10、js: retryCount < 3。切勿使用 js: 检查 DOM,例如 js: document.querySelector('.modal') !== null。
  • 如果需要基于 JavaScript 检查 DOM,请使用 CODE: 执行检查并存储结果,或使用带有 js: 的 VERIFY:(至少失败时会触发 AI 回退)。
等待语法
  • WAIT_UNTIL:——AI 会反复检查条件,直到条件满足或超时。默认超时时间为 60 秒。每次 AI 检查需要 5–10 秒,因此请将 timeout_seconds 设为至少 15。
  • WAIT:——固定时长的暂停。使用 seconds: 设置时长。

何时使用每种语法,请参阅 E2E 测试设计中的智能等待。

通用约定
  • 为提高可读性,在 ACTION 语句中将 intent 放在首位
  • 仅当 ACTION 既没有 locator 也没有 js 时,才需要使用 xpath。
  • 单测试文件、套件与参数:
    • 单测试文件——一个独立测试,不共享状态
    • 套件——存在顺序依赖的测试(例如,测试 A 创建文件,测试 B 使用该文件)。套件中的每个测试仍只覆盖一个用户流程;套件仅保证执行顺序并共享浏览器状态。请勿使用套件打包互不相关的测试。
    • 参数——相同的测试结构,不同的数据输入

E2E 测试设计最佳实践

这些原则规定了测试的内容和测试的结构,与 YAML 格式无关。请在第 2 阶段(指定)和第 4 阶段(实现)中应用这些原则。

测试隔离

每个测试都必须能够独立运行——绝不能依赖其他测试的副作用、执行顺序或遗留状态。如果测试需要数据,就由测试自行创建。

# BAD: depends on a previous test having created "My Project"
test: Delete a project
steps:
  - URL: /projects
  - intent: Click on "My Project"
    action: click
    locator: "getByText('My Project')"
  - intent: Click the Delete button
    action: click
    locator: "getByRole('button', { name: 'Delete' })"

# GOOD: creates its own data, then tests the behavior
test: Delete a project
steps:
  - CODE:
    js: |
      const res = await page.request.post('/api/projects', {
        data: { name: 'Delete-Test-' + Date.now() }
      });
      const project = await res.json();
      save_variable('projectName', project.name);
  - URL: /projects
  - WAIT_UNTIL: The project list has loaded
  - intent: Click on the project we just created
    action: click
    js: "await page.getByText('{{projectName}}').click()"
  - intent: Click the Delete button
    action: click
    locator: "getByRole('button', { name: 'Delete' })"
  - VERIFY: The project is no longer visible in the list
每个测试对应一个用户流程

每个测试都应验证一个逻辑上的用户流程。如果 8 个步骤中的第 3 步失败,第 4 到第 8 步就无法提供任何信息。请将较长的流程拆分为聚焦的测试。

**例外:**套件允许测试之间存在顺序依赖(例如,测试 A 上传文件,测试 B 下载文件)。套件中的每个测试仍只覆盖一个用户流程;套件仅保证执行顺序并共享浏览器状态。

# BAD: tests login, settings change, AND deletion in one test
test: Full user lifecycle
steps:
  - intent: Log in
  - intent: Navigate to settings
  - intent: Change display name
  - VERIFY: Name updated
  - intent: Navigate to account
  - intent: Delete account
  - VERIFY: Account deleted

# GOOD: separate tests, each verifiable in isolation
# File: update-display-name.test.yaml
test: Update display name from settings
steps:
  - URL: /settings
  - intent: Clear the display name field and type "New Name"
    action: fill
    locator: "getByLabel('Display name')"
    value: "New Name"
  - intent: Click Save
    action: click
    locator: "getByRole('button', { name: 'Save' })"
  - VERIFY: Success message "Settings saved" is visible

# File: delete-account.test.yaml (separate test)
test: Delete account from account page
steps:
  - URL: /account
  # ... focused on deletion only
断言用户看到的内容,而非实现细节

测试可见结果——文本、导航、启用/禁用状态。绝不要断言 CSS 类、数据属性、内部状态或 DOM 结构。

# BAD: asserts implementation details
- VERIFY:
    js: |
      const el = await page.locator('.btn-primary');
      await expect(el).toHaveClass(/disabled/);
      await expect(el).toHaveAttribute('data-state', 'submitted');

# GOOD: asserts what a user would observe
- VERIFY: The Submit button is disabled
    js: |
      await expect(page.getByRole('button', { name: 'Submit' }))
        .toBeDisabled({ timeout: 2000 });
聚焦的断言

验证能够证明功能正常的一件事。过多的断言会让测试变得脆弱——与被测行为无关的外观变化也会导致测试失败。

# BAD: asserts every field on the page — breaks when any label changes
- VERIFY: Page title is "Dashboard"
- VERIFY: Welcome message shows username
- VERIFY: Sidebar has 5 menu items
- VERIFY: Footer shows current year
- VERIFY: Avatar image is loaded
- VERIFY: Notification bell is visible

# GOOD: asserts the one thing that proves the user landed on the dashboard
- VERIFY: Dashboard page shows the welcome message with the user's name
绝不测试第三方服务

不要断言 Stripe 的结账流程、Google OAuth 的授权页面或 Twilio 的短信发送是否正常。应在网络边界模拟外部服务。测试你自己的集成,而非第三方的 UI。

# BAD: tests Stripe's UI (will break when Stripe updates their page)
- intent: Enter card number in Stripe iframe
- intent: Click Stripe's pay button
- VERIFY: Stripe shows success checkmark

# GOOD: mock the payment API, test your success handling
- CODE:
    js: |
      await page.route('**/api/payments', route =>
        route.fulfill({ status: 200, json: { status: 'succeeded', id: 'pi_mock' } })
      );
- intent: Click the Pay button
  action: click
  locator: "getByRole('button', { name: 'Pay' })"
- VERIFY: Order confirmation page shows "Payment successful"
确定性测试数据

每次测试运行都使用唯一标识符,避免冲突。切勿依赖其他测试或用户可能修改的硬编码数据。

# BAD: hardcoded name — collides if tests run in parallel or data persists
- intent: Type "Test User" into the name field
  action: fill
  locator: "getByLabel('Name')"
  value: "Test User"

# GOOD: unique per run — no collisions
- CODE:
    js: "save_variable('testName', 'Test-User-' + Date.now());"
- intent: Type the generated name into the name field
  action: fill
  locator: "getByLabel('Name')"
  text: "{{testName}}"
优先通过 API 初始化,而不是通过 UI 设置

如果测试需要满足前置条件(例如用户已存在、项目已创建),请通过 API 调用完成设置,而不是通过 UI 点击操作。通过 UI 设置速度慢、容易出错,而且测试的也不是这部分内容。

# BAD: 10 UI steps just to set up data before the real test
- URL: /projects/new
- intent: Type project name
- intent: Select team
- intent: Click Create
- WAIT_UNTIL: Project page loads
# ... now the actual test starts

# GOOD: API seed in one step, then test the real behavior
- CODE:
    js: |
      const res = await page.request.post('/api/projects', {
        data: { name: 'Seed-' + Date.now(), team: 'engineering' }
      });
      const { slug } = await res.json();
      save_variable('projectSlug', slug);
- URL: /projects/{{projectSlug}}/settings
- WAIT_UNTIL: Settings page has loaded
# ... test starts immediately at the point that matters
合理等待

根据具体情况选择合适的等待方式。WAIT_UNTIL: 每次检查需要 5-10s(AI 解析),因此不适合用于短暂、可预测的延迟。延迟短且已知时,使用 WAIT: 没问题。反面做法是在延迟不可预测时,用 WAIT: 代替基于条件的等待。

# BAD: guessing how long a data fetch takes — too short in CI, too long locally
- WAIT: Wait for data to load
  seconds: 5
- VERIFY: The table shows results

# GOOD: condition-based wait for unpredictable operations
- WAIT_UNTIL: The data table has at least one row visible
  timeout_seconds: 30

# ALSO GOOD: short WAIT for known, fast delays (animations, transitions, debounce)
- intent: Type search query
  action: fill
  locator: "getByRole('searchbox')"
  value: "test"
- WAIT: Wait for debounce to fire
  seconds: 1
- VERIFY: Search suggestions are visible

经验法则:如果延迟可预测且少于 5s(动画、防抖、过渡),使用 WAIT:。如果延迟不可预测(API 调用、数据加载、文件处理),使用 WAIT_UNTIL:。

测试错误状态,而不只是正常流程

真实用户会遇到错误。只覆盖正常流程的测试套件会带来虚假的信心。对于每个关键用户旅程,至少加入一个错误或边界情况测试。

# Covers: empty state, invalid input, network failure
test: Search handles no results gracefully
steps:
  - URL: /search
  - intent: Type a query that returns no results
    action: fill
    locator: "getByRole('searchbox')"
    value: "zzz_no_match_zzz"
  - intent: Submit the search
    action: click
    locator: "getByRole('button', { name: 'Search' })"
  - VERIFY: Empty state message "No results found" is displayed
  - VERIFY: The search box still contains the query (user can refine)
为并行执行做好设计

修改共享全局状态(例如全站设置、唯一的管理员账户)的测试无法安全地并行运行。请按以下方式设计:

  • 使用每个测试专属的唯一数据,而不是共享固定数据
  • 避免更改全局配置的测试
  • 如果测试必须修改共享状态,请记录此情况,并标记为串行执行
不稳定测试处理策略

重试后通过的测试仍然有问题。切勿通过添加重试来掩盖不稳定性——请找到并修复根本原因:

  • 时序不稳定? → 针对正确的条件添加恰当的 WAIT_UNTIL:
  • 数据不稳定? → 使用唯一测试数据,并添加恰当的清理操作
  • 顺序不稳定? → 测试隐式依赖了另一个测试——让其能够独立运行
  • 环境不稳定? → 模拟不稳定的外部服务

项目结构

my-tests/
├── test-specs/                   # Spec artifacts (version-controlled)
│   ├── test-strategy.md          # Phase 1: app & risk profile
│   ├── test-spec.md              # Phase 2: Given/When/Then scenarios
│   └── test-plan.md              # Phase 3: implementation plan
│
├── playwright.config.ts
├── package.json
├── .env                          # API keys + credentials (gitignored)
├── .gitignore
│
├── tests/
│   ├── public-app/               # No login needed
│   │   ├── search.test.yaml
│   │   └── filter.test.yaml
│   │
│   └── my-saas-app/              # Requires login
│       ├── auth.setup.ts         # Playwright login setup — you write this
│       ├── dashboard.test.yaml
│       └── settings.test.yaml

test-specs/ 目录包含人类可读、可进行版本控制的 Markdown 产物。不要将 test-specs/ 添加到 .gitignore。

提示

  • 带定位器的 ACTION 语句回放速度约为 DRAFT 的 10 倍。始终优先使用 ACTION。
  • 使用 inspect_page 了解页面状态。务必先读取 DOM 文件——其中提供了 act 所需的元素索引,而且消耗的令牌要少得多。只有在确实需要视觉信息(布局、颜色、图像)时才查看屏幕截图,因为截图消耗的令牌远多于 DOM。
  • 使用以下命令运行指定项目的测试:npx shiplight test --headed my-saas-app/
  • shiplightConfig() 会自动发现 .env 文件——无需手动设置 dotenv。

评论

支持 Markdown