cypress-agent-skill について
Cypress の E2E テストやコンポーネントテストを作成、デバッグ、保守する開発者やコーディングエージェント向けのリファレンスです。セレクター、ネットワークスタブ、認証、CI、テストの不安定さへの対策をまとめています。
主な機能
- 01Cypress のセットアップと設定
- 02E2E・コンポーネントテストのパターン
- 03cy.intercept によるネットワークスタブ
- 04cy.session による認証
- 05CI ガイドとテスト安定化
活用シーン
- 01新規プロジェクトへの Cypress 導入
- 02不安定な E2E テストの調査
- 03テスト内での API レスポンスのスタブ
- 04CI での 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)の実装 - Page Object Model アーキテクチャの構築
クイックスタート:
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. Page Object Model
// 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
スモークテストのタグ
// 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— カスタムコマンドライブラリ