Cypress in CI — GitHub Actions, Artifacts and Parallel Execution

Running Cypress in CI requires starting both the Angular dev server and the ASP.NET Core API before tests run, then executing Cypress headlessly. GitHub Actions provides all the infrastructure needed — Node.js for Angular and Cypress, .NET SDK for the API, Chrome for headless execution. Cypress Cloud adds parallel execution across multiple agents, dramatically reducing the total E2E test time for larger test suites.

GitHub Actions CI for Cypress

// ── .github/workflows/e2e.yml ─────────────────────────────────────────────
// name: E2E Tests
// on: [push, pull_request]
// jobs:
//   e2e:
//     runs-on: ubuntu-latest
//     steps:
//       - uses: actions/checkout@v4
//
//       - uses: actions/setup-node@v4
//         with: { node-version: '20' }
//
//       - uses: actions/setup-dotnet@v4
//         with: { dotnet-version: '8.0.x' }
//
//       - name: Restore .NET packages
//         run: dotnet restore
//
//       - name: Build .NET API
//         run: dotnet build --no-restore
//
//       - name: Start API server
//         run: dotnet run --project BlogApp.Api &
//         env:
//           ASPNETCORE_URLS: http://localhost:5000
//           ConnectionStrings__Default: ${{ secrets.TEST_DB_CONNECTION }}
//
//       - name: Install Angular dependencies
//         run: npm ci
//
//       - uses: cypress-io/github-action@v6
//         with:
//           start: ng serve --configuration=test
//           wait-on: 'http://localhost:4200, http://localhost:5000/api/health'
//           wait-on-timeout: 120
//           browser: chrome
//           record: true          # Requires CYPRESS_RECORD_KEY secret
//           parallel: true        # Requires Cypress Cloud plan
//         env:
//           CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
//           CYPRESS_ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }}
//
//       - uses: actions/upload-artifact@v4
//         if: failure()
//         with:
//           name: cypress-screenshots
//           path: cypress/screenshots
//
//       - uses: actions/upload-artifact@v4
//         if: always()
//         with:
//           name: cypress-videos
//           path: cypress/videos

// ── Cypress Cloud parallel configuration ──────────────────────────────────
// cypress.config.ts:
// export default defineConfig({
//   projectId: 'abc123',  // from Cypress Cloud
//   e2e: {
//     ...
//   },
// });

// The CI matrix runs 3 agents in parallel:
// strategy:
//   matrix:
//     containers: [1, 2, 3]
// Each agent runs a subset of spec files (balanced by Cypress Cloud)

Cypress Component Testing

// ── cypress.config.ts — add component testing ─────────────────────────────
import { defineConfig } from 'cypress';

export default defineConfig({
  component: {
    devServer: {
      framework: 'angular',
      bundler:   'webpack',
    },
    specPattern: '**/*.cy.ts',   // matches component specs too
  },
  e2e: { /* ... existing e2e config ... */ },
});

// ── PostCardComponent.cy.ts — component test with Cypress ─────────────────
import { mount } from 'cypress/angular';
import { PostCardComponent } from './post-card.component';

describe('PostCardComponent', () => {
  const post: PostSummaryDto = {
    id: 1, title: 'Test Post', slug: 'test-post',
    viewCount: 1234, authorName: 'Alice',
    publishedAt: '2024-07-15T00:00:00Z',
  };

  it('displays post title and author', () => {
    mount(PostCardComponent, {
      componentProperties: { post },
    });

    cy.get('[data-cy="post-title"]').should('contain', 'Test Post');
    cy.get('[data-cy="author-name"]').should('contain', 'Alice');
    cy.get('[data-cy="view-count"]').should('contain', '1,234');
  });

  it('navigates to post on card click', () => {
    mount(PostCardComponent, {
      componentProperties: { post },
      imports: [RouterTestingModule.withRoutes([])],
    });

    cy.get('[data-cy="read-more-btn"]').click();
    cy.location('pathname').should('eq', '/posts/test-post');
  });
});
Note: wait-on in the GitHub Action pauses the Cypress step until both servers respond with a successful health check. Without wait-on, Cypress might start before the Angular dev server has compiled and the ASP.NET Core API has finished initializing — causing the first few tests to fail with connection errors. The wait-on-timeout: 120 allows up to 2 minutes for both servers to start — more than enough for a typical compilation and startup.
Tip: Cypress Component Testing fills the gap between unit tests (TestBed with jsdom) and E2E tests (full browser + servers). It mounts individual Angular components in a real browser with the full Cypress interaction API — you can interact with the component exactly as you would in an E2E test, but without a running API server. This is ideal for testing complex UI components in isolation: drag-and-drop zones, rich text editors, data tables — anything where the real browser’s rendering and interaction model matters more than jsdom simulation.
Warning: Cypress Cloud requires a paid plan for parallel execution beyond the free tier limits. For small teams or open-source projects, consider running specs sequentially in CI even if it takes longer. Alternatively, split specs manually across matrix agents without Cypress Cloud by using --spec patterns: Agent 1 runs --spec 'cypress/e2e/public/**', Agent 2 runs --spec 'cypress/e2e/admin/**'. This is less optimal than Cypress Cloud’s intelligent balancing but requires no subscription.

Complete CI Pipeline Summary

Stage Type Tool Time
Unit tests (.NET) Unit xUnit + Moq ~30s
Unit tests (Angular) Unit Jest ~20s
Integration tests Integration WebApplicationFactory ~2min
E2E tests E2E Cypress ~5min (parallel)
Total CI time ~8min

Common Mistakes

Mistake 1 — No wait-on for server startup (Cypress starts before servers ready)

❌ Wrong — no wait-on; Cypress visits / before Angular compiles; GET http://localhost:4200 fails; all tests fail.

✅ Correct — wait-on: 'http://localhost:4200, http://localhost:5000/api/health'; Cypress only starts after both servers respond.

Mistake 2 — Missing screenshot artifacts on failure (no evidence of what failed)

❌ Wrong — no artifact upload; test fails in CI; no screenshot to diagnose what the UI showed at failure.

✅ Correct — actions/upload-artifact with if: failure() for screenshots; always visible in GitHub Actions run.

🧠 Test Yourself

Cypress Cloud parallel execution uses 3 agents. The suite has 30 spec files. How does Cypress Cloud distribute them?