Cypress is a JavaScript E2E testing framework that runs in the same browser context as the application — not in a separate Selenium-controlled browser. This architecture means Cypress has direct access to the DOM, network requests, and the application’s JavaScript environment. For the BlogApp, Cypress tests verify the complete user experience: Angular rendering, HTTP calls to the ASP.NET Core API, and real database state — exactly as a user would experience it.
Cypress Setup and First Test
// ── Installation ──────────────────────────────────────────────────────────
// npm install --save-dev cypress @types/cypress
// npm install --save-dev start-server-and-test // start servers before tests
// ── cypress.config.ts ─────────────────────────────────────────────────────
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:4200', // Angular dev server
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 8000, // wait up to 8s for elements
requestTimeout: 10000, // wait up to 10s for API calls
responseTimeout: 30000,
video: true, // record video in CI
screenshotOnRunFailure: true, // screenshot on failure
retries: {
runMode: 2, // retry failed tests up to 2 times in CI
openMode: 0, // no retries in interactive mode
},
specPattern: 'cypress/e2e/**/*.cy.ts',
supportFile: 'cypress/support/e2e.ts',
env: {
apiUrl: 'http://localhost:5000',
},
},
});
// ── cypress/support/e2e.ts — global setup ─────────────────────────────────
import './commands';
// Import Angular Testing Library queries (optional but useful)
import '@testing-library/cypress/add-commands';
// Prevent uncaught exception failures from Angular zone errors in tests
Cypress.on('uncaught:exception', (err) => {
// Ignore Angular zone errors that don't affect the test
if (err.message.includes('NG0') || err.message.includes('zone')) {
return false;
}
return true;
});
// ── cypress/support/commands.ts ───────────────────────────────────────────
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable;
loginAsAdmin(): Chainable;
getByCy(selector: string): Chainable;
createPost(post: Partial<CreatePostRequest>): Chainable<PostDto>;
}
}
}
Cypress.Commands.add('getByCy', (selector: string) =>
cy.get(`[data-cy="${selector}"]`));
Cypress.Commands.add('loginAsAdmin', () => {
cy.session('admin-session', () => {
cy.visit('/auth/login');
cy.getByCy('email-input').type('admin@test.com');
cy.getByCy('password-input').type('Admin!123Test');
cy.getByCy('login-button').click();
cy.url().should('not.include', '/auth/login');
});
});
// ── First E2E test ────────────────────────────────────────────────────────
// cypress/e2e/home.cy.ts
describe('Home page', () => {
it('shows the published posts list', () => {
cy.visit('/');
cy.title().should('include', 'BlogApp');
cy.getByCy('post-card').should('have.length.gt', 0);
cy.getByCy('post-card').first().within(() => {
cy.get('h2').should('be.visible');
cy.get('.author-name').should('be.visible');
});
});
it('shows empty state when no posts match filter', () => {
cy.visit('/?category=nonexistent-category');
cy.getByCy('empty-state').should('be.visible');
cy.getByCy('empty-state').should('contain', 'No posts found');
});
});
// ── package.json — npm scripts ─────────────────────────────────────────────
// "scripts": {
// "cy:open": "cypress open",
// "cy:run": "cypress run",
// "e2e": "start-server-and-test
// 'ng serve' 4200
// 'start-server-and-test
// \"dotnet run --project BlogApp.Api\" 5000
// cy:run'"
// }
Cypress.on('uncaught:exception') handler prevents Angular zone.js errors from failing Cypress tests. Angular may throw zone errors when tests navigate quickly, when async operations are interrupted by navigation, or when change detection runs during Cypress interactions. These are usually not test-relevant errors — the test verifies what the user sees, not Angular’s internal error count. Be selective: only ignore Angular-specific error patterns and let genuine application errors fail the test.cy.session() to cache authentication state across tests in a spec file. Without sessions, every test that requires login must navigate to the login page, enter credentials, and wait for the redirect — adding 2-5 seconds per test. With cy.session('admin-session', setupFn), the login flow runs once per spec file and the session (cookies, localStorage) is restored for subsequent tests. The session is invalidated when the setupFn changes, ensuring it re-authenticates when credentials or logic change.cypress.env.json (gitignored) or as CI environment variables. Access them in tests with Cypress.env('ADMIN_PASSWORD'). The cypress.env.json file should be in .gitignore alongside appsettings.Development.json and other local configuration files. In CI, set CYPRESS_ADMIN_PASSWORD as a secret environment variable — Cypress automatically maps CYPRESS_* env vars to Cypress.env().Common Mistakes
Mistake 1 — Hardcoded credentials in spec files (security risk)
❌ Wrong — cy.type('Admin!123Test') in a spec file committed to Git; credentials exposed.
✅ Correct — cy.type(Cypress.env('ADMIN_PASSWORD')); env var from gitignored cypress.env.json or CI secrets.
Mistake 2 — Not using cy.session() for authenticated tests (slow)
❌ Wrong — login via UI in every beforeEach; 20 tests × 3 seconds login = 60 seconds overhead.
✅ Correct — cy.session() caches session; login runs once; subsequent tests restore session instantly.