mcpField
C
cypress-agent-skill

cypress-agent-skill

github.com/kahlilr23测试与质量374 次安装34 周前更新
代码仓库

关于 cypress-agent-skill

面向开发者和编码 Agent 的 Cypress 参考技能,涵盖端到端及组件测试的编写、调试与维护,并整理了选择器、网络模拟、认证和 CI 等实践。

主要功能

  1. 01Cypress 安装与配置
  2. 02端到端及组件测试模式
  3. 03使用 cy.intercept 模拟网络
  4. 04使用 cy.session 处理认证
  5. 05CI 指南与减少测试抖动

适用场景

  1. 01在新项目中配置 Cypress
  2. 02排查不稳定的端到端测试
  3. 03在测试中模拟 API 响应
  4. 04配置 CI 中的 Cypress 运行

安装

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

$npx skills add https://github.com/kahlilr23/cypress-agent-skill --skill cypress-agent-skill

安装后在 Claude Code 中输入/cypress-agent-skill确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

install.sh
CONTRIBUTING.md
README.md
LICENSE
.github/workflows/
validate.yml
examples/
page-objects.cy.js
custom-commands.js
api-intercept.cy.js
auth-flow.cy.js
references/
assertions.md
network.md
component-testing.md
commands.md
config.md
ci.md
patterns.md
selectors.md

详细介绍

Cypress 专家技能

快速参考

何时使用此技能:

  • 编写或修复 Cypress E2E 或组件测试
  • 在新项目中设置 Cypress
  • 调试不稳定的测试
  • 添加网络存根 / API 模拟
  • 为 Cypress 配置 CI 流水线
  • 实现身份验证模式(cy.session)
  • 构建页面对象模型架构

快速入门:

  1. npm install --save-dev cypress — 安装
  2. npx cypress open — 交互模式(首次运行时会生成配置)
  3. npx cypress run — 无头 CI 模式
  4. 如需深入了解模式,请阅读 {baseDir}/references/ 中的完整参考资料

核心理念

Cypress 在浏览器内部运行。它可以原生访问 DOM、网络请求和应用状态。每条命令都会自动重试,直到成功或超时。这意味着:

  • 绝不要使用 cy.wait(3000) — 应改用别名 + cy.wait('@alias')
  • 操作后绝不要立即查询 DOM — Cypress 会自动重试
  • 始终断言结果,而非实现细节 — 测试用户可见的行为
  • 使用 data-testid 属性 — 让测试与样式 / 结构解耦

1. 安装与配置

安装
npm install --save-dev cypress
# or
yarn add -D cypress
# or
pnpm add -D cypress
cypress.config.js(JavaScript)
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: false,
    screenshotOnRunFailure: true,
    defaultCommandTimeout: 8000,
    requestTimeout: 10000,
    responseTimeout: 10000,
    retries: {
      runMode: 2,
      openMode: 0,
    },
    // v15.10.0+ — enforce new cy.env() / Cypress.expose() APIs
    // set after migrating all Cypress.env() calls
    allowCypressEnv: false,
    // v15.x — faster visibility checks
    experimentalFastVisibility: true,
    // v15.9.0+ — run all specs without --parallel flag; now works for component tests too
    experimentalRunAllSpecs: true,
    setupNodeEvents(on, config) {
      return config
    },
  },
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite',
    },
    experimentalRunAllSpecs: true,
  },
})
cypress.config.ts(TypeScript)
import { defineConfig } from 'cypress'

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.ts',
    setupNodeEvents(on, config) {
      return config
    },
  },
})
Cypress 的 tsconfig
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es5", "dom"],
    "types": ["cypress", "node"]
  },
  "include": ["**/*.ts"]
}

2. 选择器(稳定性层级)

使用可用的最稳定选择器。优先级如下:

// ✅ BEST — semantic, decoupled from style/structure
cy.get('[data-testid="submit-button"]')
cy.get('[data-cy="login-form"]')
cy.get('[data-test="user-email"]')

// ✅ GOOD — ARIA/accessibility selectors
cy.get('[role="dialog"]')
cy.get('[aria-label="Close modal"]')
cy.get('button[type="submit"]')

// ✅ GOOD — cy.contains for text-driven queries
cy.contains('button', 'Submit')
cy.contains('[data-testid="nav"]', 'Dashboard')

// ⚠️ FRAGILE — CSS classes tied to styling
cy.get('.btn-primary')         // avoid
cy.get('.MuiButton-root')      // avoid

// ❌ WORST — absolute XPath / positional
cy.get('div > ul > li:nth-child(3) > a')  // never
限定范围的查询
cy.get('[data-testid="user-card"]').within(() => {
  cy.get('[data-testid="user-name"]').should('contain', 'Alice')
  cy.get('[data-testid="user-role"]').should('contain', 'Admin')
})

cy.get('table').find('tr').should('have.length', 5)

3. 断言

Should / Expect
// Chainable assertions
cy.get('[data-testid="title"]').should('be.visible')
cy.get('[data-testid="title"]').should('have.text', 'Dashboard')
cy.get('[data-testid="title"]').should('contain.text', 'Dash')

// Multiple assertions (all retry together)
cy.get('[data-testid="btn"]')
  .should('be.visible')
  .and('not.be.disabled')
  .and('have.attr', 'type', 'submit')

// Value
cy.get('input[name="email"]').should('have.value', '[email protected]')

// Length assertions
cy.get('[data-testid="item"]').should('have.length', 3)
cy.get('[data-testid="item"]').should('have.length.greaterThan', 0)

// Negative assertions (use carefully — can pass too early)
cy.get('[data-testid="error"]').should('not.exist')
cy.get('[data-testid="spinner"]').should('not.be.visible')

// BDD expect style
cy.get('[data-testid="count"]').invoke('text').then((text) => {
  expect(parseInt(text)).to.be.greaterThan(0)
})

// URL assertions
cy.url().should('include', '/dashboard')
cy.url().should('eq', 'http://localhost:3000/dashboard')

// Alias + should
cy.get('[data-testid="price"]').invoke('text').as('price')
cy.get('@price').should('match', /\$\d+\.\d{2}/)
异步状态断言
// Wait for element to appear (retries automatically)
cy.get('[data-testid="success-message"]', { timeout: 10000 })
  .should('be.visible')

// Wait for element to disappear
cy.get('[data-testid="loading-spinner"]').should('not.exist')

4. 使用 cy.intercept 设置网络存根

// Basic stub
cy.intercept('GET', '/api/users', {
  statusCode: 200,
  body: [
    { id: 1, name: 'Alice', role: 'admin' },
    { id: 2, name: 'Bob', role: 'user' },
  ],
}).as('getUsers')

cy.visit('/users')
cy.wait('@getUsers')
cy.get('[data-testid="user-row"]').should('have.length', 2)

// Fixture file
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')

// Glob/regex patterns
cy.intercept('GET', '/api/users/*').as('getUser')
cy.intercept('GET', /\/api\/products\/\d+/).as('getProduct')

// Dynamic handler
cy.intercept('POST', '/api/orders', (req) => {
  req.reply({ statusCode: 201, body: { id: 999, ...req.body } })
}).as('createOrder')

// Modify real server response (spy + transform)
cy.intercept('GET', '/api/config', (req) => {
  req.reply((res) => {
    res.body.featureFlag = true
    return res
  })
}).as('getConfig')

// Error simulation
cy.intercept('GET', '/api/critical', { forceNetworkError: true }).as('networkError')
cy.intercept('GET', '/api/data', { statusCode: 500, body: { error: 'Server Error' } }).as('serverError')

// Delay (for loading state tests)
cy.intercept('GET', '/api/data', (req) => {
  req.reply({ delay: 1000, body: { data: [] } })
}).as('slowRequest')

// Assert request details
cy.wait('@createOrder').then((interception) => {
  expect(interception.request.body).to.deep.include({ quantity: 2 })
  expect(interception.response.statusCode).to.equal(201)
})

5. 身份验证模式

cy.session — 缓存身份验证状态(推荐)
Cypress.Commands.add('loginByUI', (email, password) => {
  cy.session(
    [email, password],
    () => {
      cy.visit('/login')
      cy.get('[data-testid="email"]').type(email)
      cy.get('[data-testid="password"]').type(password)
      cy.get('[data-testid="submit"]').click()
      cy.url().should('include', '/dashboard')
    },
    {
      validate() {
        cy.getCookie('session_token').should('exist')
      },
      cacheAcrossSpecs: true,
    }
  )
})
基于 API 的身份验证(更快)
Cypress.Commands.add('loginByApi', (email, password) => {
  cy.session(
    ['api', email, password],
    () => {
      cy.request({
        method: 'POST',
        url: '/api/auth/login',
        body: { email, password },
      }).then(({ body }) => {
        window.localStorage.setItem('auth_token', body.token)
        cy.setCookie('session', body.sessionId)
      })
    },
    {
      validate() {
        cy.window().its('localStorage').invoke('getItem', 'auth_token').should('exist')
      },
    }
  )
})

// Usage — cy.env() for secrets (v15.10.0+, replaces deprecated Cypress.env())
beforeEach(() => {
  cy.env(['adminPassword']).then(({ adminPassword }) => {
    cy.loginByApi('[email protected]', adminPassword)
    cy.visit('/dashboard')
  })
})

6. 自定义命令

// cypress/support/commands.js
Cypress.Commands.add('getByTestId', (testId, options) => {
  return cy.get(`[data-testid="${testId}"]`, options)
})

Cypress.Commands.add('waitForToast', (message) => {
  const selector = '[data-testid="toast"], [role="status"]'
  if (message) {
    cy.get(selector, { timeout: 10000 }).should('contain', message)
  } else {
    cy.get(selector, { timeout: 10000 }).should('be.visible')
  }
})

Cypress.Commands.add('fillForm', (data) => {
  Object.entries(data).forEach(([field, value]) => {
    cy.get(`[name="${field}"]`).clear().type(String(value))
  })
})

// TypeScript — cypress/support/index.d.ts
declare global {
  namespace Cypress {
    interface Chainable {
      getByTestId(testId: string, options?: Partial<Loggable & Timeoutable>): Chainable<JQuery>
      loginByApi(email: string, password: string): Chainable<void>
      loginByUI(email: string, password: string): Chainable<void>
      waitForToast(message?: string): Chainable<void>
      fillForm(data: Record<string, string | number>): Chainable<void>
    }
  }
}

7. 页面对象模型

// cypress/pages/LoginPage.js
class LoginPage {
  visit() { cy.visit('/login'); return this }
  getEmailInput() { return cy.get('[data-testid="email-input"]') }
  getPasswordInput() { return cy.get('[data-testid="password-input"]') }
  getSubmitButton() { return cy.get('[data-testid="submit-button"]') }
  getErrorMessage() { return cy.get('[data-testid="error-message"]') }

  login(email, password) {
    this.getEmailInput().clear().type(email)
    this.getPasswordInput().clear().type(password)
    this.getSubmitButton().click()
    return this
  }

  assertLoggedIn() { cy.url().should('include', '/dashboard'); return this }
  assertError(message) { this.getErrorMessage().should('contain', message); return this }
}

export default new LoginPage()

// Usage
import loginPage from '../pages/LoginPage'

it('logs in successfully', () => {
  loginPage.visit().login('[email protected]', 'password123').assertLoggedIn()
})

8. 组件测试

// cypress/component/Button.cy.jsx
import { mount } from 'cypress/react'
import Button from '../../src/components/Button'

describe('Button', () => {
  it('calls onClick when clicked', () => {
    const onClick = cy.stub().as('clickHandler')
    mount(<Button label="Submit" onClick={onClick} />)
    cy.get('button').click()
    cy.get('@clickHandler').should('have.been.calledOnce')
  })

  it('is disabled when loading', () => {
    mount(<Button label="Submit" loading={true} />)
    cy.get('button').should('be.disabled')
  })
})

// Run component tests
// npx cypress open --component
// npx cypress run --component

9. 常见模式

// Forms
cy.get('input[name="email"]').clear().type('[email protected]')
cy.get('select[name="country"]').select('United States')
cy.get('[data-testid="agree"]').check()
cy.get('[data-testid="file-input"]').selectFile('cypress/fixtures/doc.pdf')

// File drag & drop
cy.get('[data-testid="drop-zone"]').selectFile('cypress/fixtures/image.png', {
  action: 'drag-drop',
})

// Modal handling
cy.get('[data-testid="open-modal"]').click()
cy.get('[role="dialog"]').should('be.visible')
cy.get('[role="dialog"]').within(() => {
  cy.get('[data-testid="confirm-btn"]').click()
})
cy.get('[role="dialog"]').should('not.exist')

// Window alerts
cy.on('window:alert', (text) => { expect(text).to.contain('Success') })
cy.on('window:confirm', () => true)

// LocalStorage / Cookies
cy.window().then((win) => { win.localStorage.setItem('key', 'value') })
cy.setCookie('session', 'abc123')
cy.clearAllCookies()
cy.clearAllLocalStorage()

// Date/Time control
cy.clock(new Date('2024-03-15'))
cy.tick(25 * 60 * 1000)  // advance 25 minutes

// Spy on methods
cy.visit('/checkout', {
  onBeforeLoad(win) { cy.spy(win.analytics, 'track').as('track') },
})
cy.get('@track').should('have.been.calledWith', 'Purchase Completed')

10. 防止测试不稳定

// ❌ FLAKY
cy.wait(2000)
cy.get('[data-testid="result"]').should('exist')

// ✅ STABLE — wait for network alias
cy.intercept('GET', '/api/results').as('getResults')
cy.get('[data-testid="search-btn"]').click()
cy.wait('@getResults')
cy.get('[data-testid="result"]').should('have.length.greaterThan', 0)

// Test isolation — reset state between tests
beforeEach(() => {
  cy.clearAllCookies()
  cy.clearAllLocalStorage()
  cy.clearAllSessionStorage()
})

// Retries config
retries: { runMode: 2, openMode: 0 }

// Per-test retry
it('critical path', { retries: 3 }, () => { ... })

11. CI / 并行化

GitHub Actions(并行矩阵)
name: Cypress Tests
on: [push, pull_request]

jobs:
  cypress-run:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        containers: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: 'npm' }
      - run: npm ci
      - run: npm start &
      - run: npx wait-on http://localhost:3000 --timeout 60000
      - uses: cypress-io/github-action@v6
        with:
          record: true
          parallel: true
          group: 'UI Tests'
          tag: ${{ github.ref_name }}
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          CYPRESS_ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }}   # accessed via cy.env()
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: cypress-screenshots
          path: cypress/screenshots
新 CLI 标志(v15.11.0)
# Don't fail the run when no tests are found (useful for conditional spec discovery)
npx cypress run --pass-with-no-tests

# Run component tests with experimentalRunAllSpecs (now works for component testing too, v15.9.0+)
npx cypress run --component
Smoke Test 标签
// Run subset of tests in CI
const isSmoke = Cypress.expose('SMOKE') === 'true'
;(isSmoke ? describe.only : describe)('Checkout', () => { ... })
// Run: CYPRESS_SMOKE=true npx cypress run
用于 CI 的 Docker Compose
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
  cypress:
    image: cypress/included:15.11.0   # updated from 13.x
    depends_on:
      - app
    environment:
      - CYPRESS_baseUrl=http://app:3000
      - CYPRESS_ADMIN_PASSWORD=${ADMIN_PASSWORD}   # passed via cy.env()
    volumes:
      - ./:/e2e
    working_dir: /e2e
    command: cypress run --browser chrome

12. 环境变量

⚠️ v15.10.0 中的破坏性变更: Cypress.env() 已弃用,并将在 Cypress 16 中移除。
请迁移到 cy.env() 来处理机密信息,并使用 Cypress.expose() 设置公开配置值。

新 API(v15.10.0+)
// cy.env() — for SECRETS (API keys, passwords, tokens)
// Async, only exposes the values you explicitly request
// Values are NOT serialized into browser state
cy.env(['apiKey', 'adminPassword']).then(({ apiKey, adminPassword }) => {
  cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { email: '[email protected]', password: adminPassword },
    headers: { Authorization: `Bearer ${apiKey}` },
  })
})

// Cypress.expose() — for NON-SENSITIVE public config
// Synchronous, safe to appear in browser state
// Use for: feature flags, API versions, env labels, base URLs
const apiUrl = Cypress.expose('apiUrl')
cy.visit(apiUrl + '/dashboard')
cypress.config.js(v15.10.0+)
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  // Enforce migration — disables legacy Cypress.env() API entirely
  allowCypressEnv: false,

  env: {
    apiUrl: 'http://localhost:3001',       // non-sensitive — use Cypress.expose()
    adminEmail: '[email protected]',          // non-sensitive — use Cypress.expose()
    // secrets (apiKey, adminPassword) come from cypress.env.json or CYPRESS_* OS vars
    // never hardcode secrets here
  },
})
cypress.env.json — 仅用于机密信息(请将此文件加入 gitignore)
{
  "adminPassword": "secret123",
  "apiKey": "test-key"
}
CI 环境变量(前缀 CYPRESS_)
# Set secrets as CI env vars — accessed via cy.env()
CYPRESS_API_KEY=abc123 npx cypress run
CYPRESS_ADMIN_PASSWORD=secret npx cypress run
迁移速查表
旧方式(已弃用)新方式适用场景
Cypress.env('apiKey')cy.env(['apiKey']).then(...)在钩子 / 测试中使用机密信息
Cypress.env('apiUrl')Cypress.expose('apiUrl')公开配置,需要同步访问
Cypress.env()(全部)绝不要使用 — 有意采用显式访问—
插件迁移说明

热门插件需要更新,以移除对 Cypress.env() 的依赖:

  • @cypress/grep → 升级到最新主版本
  • @cypress/code-coverage → 升级到最新主版本
  • 在设置 allowCypressEnv: false 之前,检查所有自定义插件是否使用了 Cypress.env()

13. 固件与数据管理

// cypress/fixtures/user.json
{
  "id": 1,
  "name": "Test User",
  "email": "[email protected]",
  "role": "admin"
}

// Load fixture
cy.fixture('user.json').then((user) => {
  cy.get('[data-testid="name"]').should('contain', user.name)
})

// Use fixture as stub (shorthand)
cy.intercept('GET', '/api/user', { fixture: 'user.json' }).as('getUser')

// Dynamic data generation
const generateUser = (overrides = {}) => ({
  id: Math.random(),
  name: 'Test User',
  email: `test+${Date.now()}@example.com`,
  role: 'user',
  ...overrides,
})

cy.intercept('GET', '/api/user', generateUser({ role: 'admin' })).as('getUser')

// Seed DB via API task (faster than UI)
beforeEach(() => {
  cy.request('POST', '/api/test/reset', { scenario: 'clean-slate' })
})

14. 无障碍测试

// Install: npm install --save-dev cypress-axe axe-core
// Add to cypress/support/e2e.js: import 'cypress-axe'

describe('Accessibility', () => {
  beforeEach(() => {
    cy.visit('/')
    cy.injectAxe()
  })

  it('has no detectable a11y violations on load', () => {
    cy.checkA11y()
  })

  it('has no violations in the modal', () => {
    cy.get('[data-testid="open-modal"]').click()
    cy.get('[role="dialog"]').should('be.visible')
    cy.checkA11y('[role="dialog"]', {
      rules: {
        'color-contrast': { enabled: false },  // disable specific rules
      },
    })
  })

  it('reports violations with details', () => {
    cy.checkA11y(null, null, (violations) => {
      violations.forEach((violation) => {
        cy.log(`${violation.id}: ${violation.description}`)
        violation.nodes.forEach((node) => cy.log(node.html))
      })
    })
  })
})

15. 最佳实践总结

模式应做不应做
选择器data-testid 属性CSS 类、XPath
等待cy.wait('@alias')cy.wait(3000)
身份验证cy.session()每次测试都通过 UI 登录
断言链式调用 .should()使用隐式 then() 检查
数据通过 API 预置数据通过 UI 设置测试数据
隔离使用 beforeEach 重置在测试之间共享状态
网络使用 cy.intercept() 设置存根在单元测试中发起真实 API 调用

16. 调试

// Pause execution — opens interactive debugger in Cypress UI
cy.pause()

// Debug current subject — logs to console
cy.get('[data-testid="el"]').debug()

// Log custom messages
cy.log('Current step: submitting checkout form')

// Take screenshot at specific point
cy.screenshot('before-submit')

// Inspect DOM state
cy.get('[data-testid="form"]').then(($el) => {
  console.log('Form HTML:', $el.html())
  debugger  // opens DevTools when running in interactive mode
})

// Time travel debugging: Cypress UI → click any command in the log → DOM snapshot appears
cy.prompt() — 实验性自然语言测试(v15.x)
// cy.prompt() lets you write tests in plain English
// Cypress AI interprets intent and generates the necessary commands
// Enable: set experimentalCyPrompt: true in cypress.config.js

cy.prompt('Click the submit button and verify the success message appears')
cy.prompt('Fill in the login form with admin credentials and sign in')
cy.prompt('Verify the product list shows 3 items and the first one is selected')

启用方式:在 cypress.config.js 中设置 experimentalCyPrompt: true。自愈功能:如果选择器发生变化,Cypress 会尝试根据意图重新定位元素,而不是立即失败。


参考资料

{baseDir}/references/ 中的所有详细参考资料:

  • selectors.md — 选择器策略和反模式
  • commands.md — 完整的 cy.* 命令速查表
  • network.md — cy.intercept 高级模式
  • assertions.md — 完整的断言参考
  • config.md — 完整的 cypress.config.js 选项
  • ci.md — CI/CD 配置指南(GitHub Actions、GitLab、CircleCI、Jenkins)
  • component-testing.md — React/Vue/Angular 组件测试
  • patterns.md — 视觉回归、API 测试、多标签页、拖放

示例

{baseDir}/examples/ 中可运行的测试文件:

  • auth-flow.cy.js — 使用 cy.session 的完整身份验证
  • api-intercept.cy.js — 网络存根模式
  • page-objects.cy.js — POM 实现
  • custom-commands.js — 自定义命令库

评论

支持 Markdown