mcpField
C
cypress-agent-skill

cypress-agent-skill

github.com/kahlilr23テスト・品質376 回インストール36 週間前に更新
リポジトリ

cypress-agent-skill について

Cypress の E2E テストやコンポーネントテストを作成、デバッグ、保守する開発者やコーディングエージェント向けのリファレンスです。セレクター、ネットワークスタブ、認証、CI、テストの不安定さへの対策をまとめています。

主な機能

  1. 01Cypress のセットアップと設定
  2. 02E2E・コンポーネントテストのパターン
  3. 03cy.intercept によるネットワークスタブ
  4. 04cy.session による認証
  5. 05CI ガイドとテスト安定化

活用シーン

  1. 01新規プロジェクトへの Cypress 導入
  2. 02不安定な E2E テストの調査
  3. 03テスト内での API レスポンスのスタブ
  4. 04CI での 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)の実装
  • Page Object Model アーキテクチャの構築

クイックスタート:

  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. 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 — カスタムコマンドライブラリ

コメント