AgentStack
SKILL verified MIT Self-run

Cypress Agent Skill

skill-kahlilr23-cypress-agent-skill-cypress-agent-skill · by KahlilR23

Production-grade Cypress E2E and component testing — selectors, network stubbing, auth, CI parallelization, flake elimination, Page Object Model, and TypeScript support. The complete Cypress skill for AI agents.

No reviews yet
0 installs
10 views
0.0% view→install

Install

$ agentstack add skill-kahlilr23-cypress-agent-skill-cypress-agent-skill

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Cypress Agent Skill? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Cypress Expert Skill

Quick Reference

When to use this skill:

  • Writing or fixing Cypress E2E or component tests
  • Setting up Cypress in a new project
  • Debugging flaky tests
  • Adding network stubbing / API mocking
  • Configuring CI pipelines for Cypress
  • Implementing auth patterns (cy.session)
  • Building Page Object Model architecture

Quick start:

  1. npm install --save-dev cypress — install
  2. npx cypress open — interactive mode (first run generates config)
  3. npx cypress run — headless CI mode
  4. Read full references in {baseDir}/references/ for deep patterns

Core Philosophy

Cypress runs inside the browser. It has native access to the DOM, network requests, and application state. Every command is automatically retried until it passes or times out. This means:

  • Never use cy.wait(3000) — use aliases + cy.wait('@alias') instead
  • Never query DOM immediately after an action — Cypress retries automatically
  • Always assert on outcomes, not implementation — test user-visible behavior
  • Use data-testid attributes — decouple tests from styling/structure

1. Installation & Configuration

Install

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
    },
  },
})

tsconfig for Cypress

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es5", "dom"],
    "types": ["cypress", "node"]
  },
  "include": ["**/*.ts"]
}

2. Selectors (Stability Hierarchy)

Use the most stable selector available. Prefer in this order:

// ✅ 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

Scoped Queries

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. Assertions

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', 'user@example.com')

// 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}/)

Async State Assertions

// 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. Network Stubbing with 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. Authentication Patterns

cy.session — Cache Auth State (Recommended)

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-Based Auth (Faster)

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('admin@example.com', adminPassword)
    cy.visit('/dashboard')
  })
})

6. Custom Commands

// 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): Chainable
      loginByApi(email: string, password: string): Chainable
      loginByUI(email: string, password: string): Chainable
      waitForToast(message?: string): Chainable
      fillForm(data: Record): Chainable
    }
  }
}

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('admin@example.com', 'password123').assertLoggedIn()
})

8. Component Testing

// 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()
    cy.get('button').click()
    cy.get('@clickHandler').should('have.been.calledOnce')
  })

  it('is disabled when loading', () => {
    mount()
    cy.get('button').should('be.disabled')
  })
})

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

9. Common Patterns

// Forms
cy.get('input[name="email"]').clear().type('new@example.com')
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. Flake Prevention

// ❌ 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 / Parallelization

GitHub Actions (Parallel Matrix)

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

New CLI flags (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 Tags

// Run subset of tests in CI
const isSmoke = Cypress.expose('SMOKE') === 'true'
;(isSmoke ? describe.only : describe)('Checkout', () => { ... })
// Run: CYPRESS_SMOKE=true npx cypress run

Docker Compose for CI

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. Environment Variables

> ⚠️ Breaking change in v15.10.0: Cypress.env() is deprecated and will be removed in Cypress 16. > Migrate to cy.env() for secrets and Cypress.expose() for public config values.

New 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: 'admin@test.com', 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: 'admin@test.com',          // non-sensitive — use Cypress.expose()
    // secrets (apiKey, adminPassword) come from cypress.env.json or CYPRESS_* OS vars
    // never hardcode secrets here
  },
})

cypress.env.json — secrets only (gitignore this file)

{
  "ad

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [KahlilR23](https://github.com/KahlilR23)
- **Source:** [KahlilR23/cypress-agent-skill](https://github.com/KahlilR23/cypress-agent-skill)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.