关于 cypress-agent-skill
面向开发者和编码 Agent 的 Cypress 参考技能,涵盖端到端及组件测试的编写、调试与维护,并整理了选择器、网络模拟、认证和 CI 等实践。
主要功能
- 01Cypress 安装与配置
- 02端到端及组件测试模式
- 03使用 cy.intercept 模拟网络
- 04使用 cy.session 处理认证
- 05CI 指南与减少测试抖动
适用场景
- 01在新项目中配置 Cypress
- 02排查不稳定的端到端测试
- 03在测试中模拟 API 响应
- 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.shCONTRIBUTING.mdREADME.mdLICENSE.github/workflows/validate.ymlexamples/page-objects.cy.jscustom-commands.jsapi-intercept.cy.jsauth-flow.cy.jsreferences/assertions.mdnetwork.mdcomponent-testing.mdcommands.mdconfig.mdci.mdpatterns.mdselectors.md详细介绍
Cypress 专家技能
快速参考
何时使用此技能:
- 编写或修复 Cypress E2E 或组件测试
- 在新项目中设置 Cypress
- 调试不稳定的测试
- 添加网络存根 / API 模拟
- 为 Cypress 配置 CI 流水线
- 实现身份验证模式(
cy.session) - 构建页面对象模型架构
快速入门:
npm install --save-dev cypress— 安装npx cypress open— 交互模式(首次运行时会生成配置)npx cypress run— 无头 CI 模式- 如需深入了解模式,请阅读
{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