cypress/e2e/intercept.cy.ts
describe('API Interception', () => {
// ── Stub Response ──
it('stubs API response', () => {
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
],
headers: { 'content-type': 'application/json' },
}).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
cy.get('[data-cy="user-list"]').should('have.length', 2);
});
// ── Dynamic Response Handler ──
it('modifies response dynamically', () => {
cy.intercept('POST', '/api/users**', (req) => {
req.reply((res) => {
res.send({
id: Date.now(),
...req.body,
createdAt: new Date().toISOString(),
});
});
}).as('createUser');
cy.get('[data-cy="name"]').type('New User');
cy.get('[data-cy="submit"]').click();
cy.wait('@createUser').its('response.statusCode').should('eq', 201);
});
// ── Spy Without Stubbing ──
it('spies on API call', () => {
cy.intercept('GET', '/api/users').as('getUsers');
cy.visit('/users');
cy.wait('@getUsers').then((interception) => {
expect(interception.response.statusCode).to.eq(200);
expect(interception.response.body).to.have.length(10);
});
});
// ── Simulate Error ──
it('tests error handling', () => {
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { error: 'Internal Server Error' },
}).as('error');
cy.visit('/users');
cy.wait('@error');
cy.get('[data-cy="error-msg"]').should('contain', 'Something went wrong');
});
// ── Delay Response ──
it('tests loading state', () => {
cy.intercept('GET', '/api/users', {
delay: 2000,
body: [{ id: 1, name: 'John' }],
}).as('slowUsers');
cy.visit('/users');
cy.get('[data-cy="skeleton"]').should('be.visible');
cy.wait('@slowUsers');
cy.get('[data-cy="skeleton"]').should('not.exist');
});
});cypress/fixtures/users.json
{
"admin": {
"email": "admin@example.com",
"password": "Admin123!",
"name": "Admin User"
},
"users": [
{ "id": 1, "name": "John Doe", "role": "user" },
{ "id": 2, "name": "Jane Smith", "role": "admin" }
],
"newUser": {
"name": "Test User",
"email": "test@example.com",
"role": "user"
}
}cypress/e2e/fixtures.cy.ts
describe('Using Fixtures', () => {
// Load fixture data for stubbing
it('loads fixture for API stub', () => {
cy.fixture('users.json').then((users) => {
cy.intercept('GET', '/api/users', { body: users.users }).as('getUsers');
});
cy.visit('/users');
cy.wait('@getUsers');
});
// Fixture alias with this binding
it('uses fixture alias', () => {
cy.fixture('admin').as('adminUser');
cy.get('[data-cy="email"]').then(function () {
cy.get('[data-cy="email"]').type(this.adminUser.email);
cy.get('[data-cy="password"]').type(this.adminUser.password);
});
});
// Override fixture data
it('overrides fixture values', () => {
cy.fixture('newUser').then((user) => {
user.name = 'Dynamic Name ' + Date.now();
cy.intercept('POST', '/api/users', { body: user }).as('create');
});
});
});// ── Custom Commands ──
// Login via UI
Cypress.Commands.add('login', (email: string, password: string) => {
cy.visit('/login');
cy.get('[data-cy="email"]').type(email);
cy.get('[data-cy="password"]').type(password);
cy.get('[data-cy="submit"]').click();
});
// Login via API (faster, skips UI)
Cypress.Commands.add('loginByApi', (email: string, password: string) => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password },
}).then((response) => {
window.localStorage.setItem('token', response.body.token);
window.localStorage.setItem('user', JSON.stringify(response.body.user));
});
});
// Session-based login (persists across tests)
Cypress.Commands.add('sessionLogin', (email: string, password: string) => {
cy.session([email, password], () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password },
}).then((response) => {
window.localStorage.setItem('token', response.body.token);
});
});
});
// Data-cy shortcut
Cypress.Commands.add('getByDataCy', (selector: string) => {
return cy.get('[data-cy="' + selector + '"]');
});
// Add type declarations
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
loginByApi(email: string, password: string): Chainable<void>;
sessionLogin(email: string, password: string): Chainable<void>;
getByDataCy(selector: string): Chainable<JQuery>;
}
}
}cypress/e2e/pom/loginPage.ts
// ── Page Object Model ──
class LoginPage {
visit() {
cy.visit('/login');
return this;
}
getEmailInput() { return cy.get('[data-cy="email"]'); }
getPasswordInput() { return cy.get('[data-cy="password"]'); }
getSubmitButton() { return cy.get('[data-cy="submit"]'); }
getErrorMessage() { return cy.get('[data-cy="error"]'); }
fillForm(email: string, password: string) {
this.getEmailInput().clear().type(email);
this.getPasswordInput().clear().type(password);
return this;
}
submit() {
this.getSubmitButton().click();
return this;
}
login(email: string, password: string) {
return this.fillForm(email, password).submit();
}
}
// Usage in tests
const loginPage = new LoginPage();
loginPage.visit().login('user@example.com', 'password123');
// ── Or export as custom command
Cypress.Commands.add('loginPage', () => loginPage);💡 Use cy.session() to cache authentication across tests within a spec. It avoids re-logging in before every test, dramatically improving test speed. Combine with cy.intercept() for API-based login.
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
video: true,
screenshotOnRunFailure: true,
defaultCommandTimeout: 10000,
responseTimeout: 30000,
requestTimeout: 5000,
retries: { runMode: 2, openMode: 0 },
experimentalOriginDependencies: true,
// Setup before tests
setupNodeEvents(on, config) {
return require('./cypress/plugins/index')(on, config);
},
// Global setup
specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
excludeSpecPattern: '**/examples/**',
env: {
apiUrl: 'http://localhost:8080/api',
userId: 'test-user',
},
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
},
});.github/workflows/cypress.yml
name: Cypress E2E Tests
on: [push, pull_request]
jobs:
cypress:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run build
- run: npx prisma migrate deploy
- run: npx prisma db seed
- uses: cypress-io/github-action@v6
with:
start: npm run start
wait-on: "http://localhost:3000"
wait-on-timeout: 60
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- 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/videosPractice Why Use data-cy attributes Decoupled from CSS/class names cy.session() for auth Skip login before each test Custom commands Reusable action sequences Fixtures for test data Separate data from tests baseUrl in config Avoid absolute URLs Page Object Model Organize locators centrally Avoid explicit waits Use auto-retry assertions Test independence Each test should run alone
Feature Cypress Playwright Browsers Chromium, Firefox, WebKit (partial) Chromium, Firefox, WebKit (full) Architecture In-browser DevTools Protocol Parallel exec Paid (Cypress Cloud) Built-in (workers) Multi-tab Limited Full support Mobile viewport emulation viewport emulation Auto-wait Built-in retry Built-in auto-wait Components Cypress Components Component Testing Learning curve Lower Moderate
cypress/e2e/utilities.cy.ts
describe('Utility Commands', () => {
// ── Cypress.env ──
it('reads environment variables', () => {
const apiUrl = Cypress.env('apiUrl');
cy.visit(apiUrl);
});
// ── cy.request (direct API calls) ──
it('seeds data via API', () => {
cy.request('POST', '/api/seed', { users: 50 });
cy.visit('/users');
cy.get('[data-cy="user-row"]').should('have.length', 50);
});
// ── cy.wrap (wrap non-Cypress objects) ──
it('wraps plain arrays', () => {
cy.wrap([1, 2, 3]).each((num) => {
cy.log('Number: ' + num);
});
});
// ── cy.task (run Node.js code in tests) ──
// cypress/plugins/index.ts:
// on('task', { seedDb: (count) => seedDatabase(count) });
it('runs Node.js task', () => {
cy.task('seedDb', 100);
});
// ── cy.debug / cy.log ──
it('debugging', () => {
cy.get('[data-cy="item"]').debug(); // pause debugger
cy.log('This is a log message');
cy.get('[data-cy="item"]').should('be.visible');
});
// ── cy.clock (control time) ──
it('tests timer-dependent behavior', () => {
cy.clock();
cy.visit('/dashboard');
cy.tick(1000); // advance 1 second
cy.get('[data-cy="notification"]').should('be.visible');
});
// ── cy.stub / cy.spy ──
it('stubs window methods', () => {
const stub = cy.stub(window, 'fetch').resolves({
json: () => Promise.resolve({ data: 'mocked' }),
});
cy.visit('/page');
cy.wrap(stub).should('have.been.calledOnce');
});
// ── Screenshot on failure ──
it('captures failure', () => {
cy.get('[data-cy="btn"]').click();
cy.screenshot('after-click');
// Cypress auto-screenshots on failure
});
});⚠️ Prefer cy.intercept() over cy.route() (deprecated). Use cy.clock() and cy.tick() to test time-dependent code without actual delays. Use cy.request() for API-level setup and seeding, avoiding slow UI-based setup.
Q: What is the Cypress Test Runner architecture? Cypress runs inside the browser alongside your application. It has direct access to the DOM, window object, network layer, and browser APIs. Unlike Selenium, there is no WebDriver — Cypress communicates directly with the browser. This gives it faster execution, consistent behavior, and the ability to read/modify application state.
Q: How does cy.intercept() work? cy.intercept() controls network traffic at the browser level. It can spy on requests (observe), stub responses (return mock data), or modify requests/responses dynamically. Define a route matcher (URL pattern) and optionally a handler function. Use .as() to alias and cy.wait() to wait for the route.
Q: What is the test retry mechanism? Cypress automatically retries assertions that are chained to commands (e.g., cy.get().should()). It retries for up to defaultCommandTimeout (4s). This eliminates explicit waits and handles async rendering. For test-level retries, configure retries: { runMode: 2 } in cypress.config.ts.
Q: How do you handle authentication in Cypress? Three approaches: (1) UI-based login in beforeEach (slow but realistic), (2) API-based login via cy.request() to get a token and set localStorage (fast, recommended), (3) cy.session() to cache auth across tests (fastest). Use API login for most cases.
Q: What are custom commands? Reusable command chains added via Cypress.Commands.add(). Defined in support/e2e.ts. Examples: cy.login(), cy.getByDataCy(), cy.drag(). Must declare types in a global module augmentation for TypeScript support. They encapsulate common action sequences.
Q: How does Cypress compare to Playwright? Cypress: in-browser architecture, easier setup, auto-retry assertions, single-tab limitation. Playwright: multi-browser (including Safari), multi-tab/page, built-in parallel execution, auto-waiting, cross-origin support. Playwright is better for complex scenarios; Cypress for developer experience.
Q: How do you test file uploads? cy.get('input[type=file]').selectFile('path/to/file.pdf') for standard file inputs. For drag-and-drop, use cy.get('.dropzone').selectFile('file.pdf', { action: 'drag-drop' }). Cypress automatically handles the file input event.
💡 Cypress interview tips: Know the architecture (runs inside browser), explain cy.intercept for network stubbing, describe custom commands and Page Object patterns, discuss cy.session for performance optimization, compare with Playwright tradeoffs, and describe real-world CI/CD integration strategies.