⏳
Loading cheatsheet...
E2E testing flows, fixtures, network mocks, retries and best practices with Cypress.
# ── Installation ──
npm install --save-dev cypress
# or
yarn add -D cypress
# Open Cypress
npx cypress open
# Run all tests (headless)
npx cypress run
# Run specific spec
npx cypress run --spec "cypress/e2e/login.cy.js"
# Run by tag/grep
npx cypress run --grep "@smoke"
# Run in specific browser
npx cypress run --browser chrome
npx cypress run --browser firefox
# Headed mode (visible browser)
npx cypress run --headed
# Record to Cypress Cloud
npx cypress run --record --key YOUR_KEY// ── cypress.config.js ──
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 10000,
requestTimeout: 5000,
responseTimeout: 30000,
video: true,
screenshotOnRunFailure: true,
trashAssetsBeforeRuns: true,
watchForFileChanges: true,
// Page load timeout
pageLoadTimeout: 60000,
// Environment variables
env: {
apiUrl: 'http://localhost:3001/api',
username: 'testuser',
password: 'testpass123',
},
// Setup node events (for custom plugins)
setupNodeEvents(on, config) {
// implement node event listeners here
// on('task', { ... })
return config;
},
},
// Component testing
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
},
});// ── Basic Test Structure ──
describe('Login Page', () => {
beforeEach(() => {
// Runs before each test
cy.visit('/login');
});
before(() => {
// Runs once before all tests
cy.log('Starting login tests');
});
afterEach(() => {
// Runs after each test
cy.url().then(url => cy.log('Test ended at:', url));
});
after(() => {
// Runs once after all tests
cy.log('Login tests complete');
});
it('should display login form', () => {
cy.get('[data-testid="email-input"]').should('be.visible');
cy.get('[data-testid="password-input"]').should('be.visible');
cy.get('button[type="submit"]').should('be.visible');
});
it('should login successfully', () => {
cy.get('[data-testid="email-input"]').type('alice@example.com');
cy.get('[data-testid="password-input"]').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.contains('Welcome back').should('be.visible');
});
it('should show error for invalid credentials', () => {
cy.get('[data-testid="email-input"]').type('wrong@test.com');
cy.get('[data-testid="password-input"]').type('wrongpass');
cy.get('button[type="submit"]').click();
cy.get('[data-testid="error-message"]')
.should('be.visible')
.should('contain', 'Invalid');
});
// Skip / only
it.skip('should remember me (skipped)', () => {});
it.only('should run this test only', () => {});
});| Hook | When | Use Case |
|---|---|---|
| before() | Once before all | Seed database |
| beforeEach() | Before each test | Login, visit page |
| afterEach() | After each test | Cleanup, screenshots |
| after() | Once after all | Close connections |
| Command | Description |
|---|---|
| cypress open | Open Test Runner GUI |
| cypress run | Run all tests headless |
| cypress run --spec | Run specific spec file |
| cypress run --headed | Visible browser mode |
| cypress run --browser | Specific browser |
| cypress run --record | Record to Cloud |
// ── Element Selection Strategies ──
// cy.get() — CSS selector (most common)
cy.get('button'); // tag
cy.get('.btn-primary'); // class
cy.get('#submit-btn'); // id
cy.get('[data-testid="login-form"]'); // data attribute
cy.get('input[name="email"]'); // attribute
cy.get('nav > ul > li'); // child combinator
cy.get('.card, .panel'); // multiple selectors
cy.get('form:first'); // pseudo-selector
cy.get('tr:nth-child(2)'); // nth child
// cy.contains() — by text content
cy.contains('Submit');
cy.contains('button', 'Login'); // text within element
cy.contains('.error', 'Invalid'); // text within class
cy.contains(/regex.*pattern/i); // regex match
// cy.root() — root element
cy.root().should('have.class', 'app-root');
// cy.within() — scope queries to a container
cy.get('.user-card').within(() => {
cy.get('h3').should('contain', 'Alice');
cy.get('.email').should('contain', 'alice@test.com');
});
// cy.closest() — traverse up DOM
cy.get('.delete-btn').closest('tr').should('have.attr', 'data-id', '1');
// cy.parent() / cy.children() / cy.siblings()
cy.get('td').parent('tr');
cy.get('ul').children('li').should('have.length', 3);
cy.get('.active').siblings('.tab');
// cy.first() / cy.last() / cy.eq()
cy.get('li').first();
cy.get('li').last();
cy.get('li').eq(2); // 0-indexed
cy.get('li').filter('.active');// ── Selector Best Practices ──
// ✅ BEST — data-cy or data-testid
cy.get('[data-cy="submit-button"]');
cy.get('[data-testid="login-form"]');
// ✅ GOOD — by role-like text in specific element
cy.contains('button', 'Save Changes');
cy.get('input[aria-label="Search"]');
// ✅ OK — specific CSS selectors
cy.get('[name="email"]');
cy.get('#unique-header');
// ❌ AVOID — brittle selectors
cy.get('div > div > ul > li:nth-child(3) > a');
cy.get('.css-1a2b3c4d'); // auto-generated classes
// ── Cypress selector playground ──
// In Test Runner: click the "select" icon, then click
// elements on the page. Cypress suggests best selectors.
// ── Filtering collections ──
// Get the specific row in a table
cy.get('table tbody tr')
.filter(':contains("Alice")')
.find('td')
.eq(2)
.should('contain', 'admin');
// Get elements that don't match
cy.get('button').not('.disabled').click();
// Get elements by multiple attributes
cy.get('input').filter('[required]').should('have.length', 3);| Method | Description |
|---|---|
| cy.get(selector) | CSS selector query |
| cy.contains(text) | Find by text content |
| cy.root() | Get root DOM element |
| cy.within(fn) | Scope queries to parent |
| cy.closest(selector) | Traverse up to ancestor |
| cy.parent() | Get parent element |
| cy.children() | Get child elements |
| cy.siblings() | Get sibling elements |
| cy.first() / cy.last() | First/last in collection |
| cy.eq(index) | Element at index |
| cy.filter(selector) | Filter by selector |
| cy.not(selector) | Exclude by selector |
| Priority | Strategy | Stability |
|---|---|---|
| 1 (best) | data-cy / data-testid | Very stable |
| 2 | aria-* attributes | Accessible, stable |
| 3 | HTML attributes (id, name) | Stable |
| 4 | Semantic selectors | Readable |
| 5 | Class/tag combos | Moderate |
| 6 (worst) | Deeply nested CSS | Brittle |
data-cy attributes as your primary selector strategy. They are stable across refactors and clearly indicate which elements are used in tests.// ── Clicking ──
cy.get('button').click();
cy.get('button').dblclick(); // double click
cy.get('button').rightclick(); // right click
cy.get('button').click({ force: true }); // skip actionability checks
cy.get('button').click({ multiple: true }); // click all matches
cy.get('button').click({ scrollBehavior: 'center' }); // scroll into view
// With position offset
cy.get('button').click('topLeft');
cy.get('button').click('topRight');
cy.get('button').click('bottomLeft');
cy.get('button').click('bottomRight');
cy.get('button').click('center');
// ── Typing ──
cy.get('input').type('hello world');
cy.get('input').type('Hello World', { delay: 50 }); // custom keystroke delay
cy.get('input').type('{enter}'); // special keys
cy.get('input').type('{ctrl}{c}'); // keyboard shortcuts
cy.get('input').type('{selectall}{del}'); // select all + delete
cy.get('input').clear(); // clear input
cy.get('input').type('text', { parseSpecialCharSequences: false });
// ── Checkbox & Radio ──
cy.get('[type="checkbox"]').check(); // check
cy.get('[type="checkbox"]').uncheck(); // uncheck
cy.get('[type="checkbox"]').check({ force: true });
cy.get('[type="radio"]').check('value'); // select radio by value
// ── Select / Dropdown ──
cy.get('select').select('apple'); // by value
cy.get('select').select('Apple Fruit'); // by visible text
cy.get('select').select([1, 3]); // multiple values
// ── File Upload ──
cy.get('input[type="file"]').selectFile('cypress/fixtures/avatar.png');
cy.get('input[type="file"]').selectFile(['file1.pdf', 'file2.pdf']);// ── Hover, Focus, Scroll ──
cy.get('.tooltip-trigger').trigger('mouseover');
cy.get('.dropdown').trigger('mouseenter');
cy.get('input').focus();
cy.get('input').blur();
cy.scrollTo('bottom');
cy.scrollTo(0, 500); // scroll to position
cy.get('.modal').scrollIntoView();
// ── Drag and Drop ──
// Using drag plugin: npm install @4tw/cypress-drag-drop
cy.get('.source').drag('.target');
// Manual approach
cy.get('.draggable')
.trigger('mousedown', { which: 1, pageX: 100, pageY: 100 })
.trigger('mousemove', { which: 1, pageX: 300, pageY: 300 })
.trigger('mouseup', { force: true });
// ── Real Events (keyboard, mouse) ──
cy.get('body').type('{ctrl}{f}'); // open find
cy.get('body').type('{esc}'); // press escape
cy.get('body').tab(); // tab key
cy.get('textarea').type('line1{enter}line2{enter}line3');
// ── Trigger Custom Events ──
cy.get('#my-element').trigger('keydown', { keyCode: 27, key: 'Escape' });
cy.get('#my-element').trigger('input', { data: 'new value' });
// ── Viewport Interactions ──
cy.viewport(320, 480); // mobile
cy.viewport('iphone-6'); // preset device
cy.viewport('macbook-15'); // preset device
cy.viewport(1280, 720, { log: false }); // custom with options| Sequence | Action |
|---|---|
| {enter} | Enter key |
| {tab} | Tab key |
| {esc} | Escape key |
| {backspace} | Backspace |
| {del} | Delete key |
| {ctrl} | Control modifier |
| {alt} | Alt modifier |
| {shift} | Shift modifier |
| {selectall} | Select all (Ctrl+A) |
| {ctrl}{c} | Copy |
| {ctrl}{v} | Paste |
| Check | Description |
|---|---|
| Visible | Element is not hidden |
| Not detached | Element is in DOM |
| Not disabled | Element is not disabled |
| Not read-only | Input is editable |
| Not animating | No running animation |
| Not covered | Not overlapped by other element |
{ force: true } to skip these checks only when necessary.// ── Implicit Assertions (.should()) ──
// Visibility
cy.get('.element').should('be.visible');
cy.get('.element').should('not.be.visible');
cy.get('.element').should('exist');
cy.get('.element').should('not.exist');
cy.get('.element').should('be.hidden');
// Content
cy.get('h1').should('have.text', 'Welcome');
cy.get('h1').should('contain', 'Welcome');
cy.get('h1').should('not.contain', 'Error');
cy.get('h1').invoke('text').should('match', /welcome/i);
cy.get('h1').should('have.html', '<span>Welcome</span>');
// Attributes & Classes
cy.get('input').should('have.attr', 'placeholder', 'Search...');
cy.get('input').should('have.attr', 'required');
cy.get('div').should('have.class', 'active');
cy.get('div').should('have.class', 'card', 'panel');
cy.get('div').should('have.id', 'main-content');
cy.get('a').should('have.css', 'color', 'rgb(59, 130, 246)');
cy.get('div').should('have.css', 'display', 'flex');
// State
cy.get('button').should('be.enabled');
cy.get('button').should('be.disabled');
cy.get('button').should('be.focused');
cy.get('input').should('be.checked');
cy.get('input').should('not.be.checked');
// Values
cy.get('input').should('have.value', 'hello');
cy.get('select').should('have.value', 'option1');
cy.get('input').should('be.empty');
// Count & Length
cy.get('li').should('have.length', 5);
cy.get('li').should('have.length.greaterThan', 2);
cy.get('li').should('have.length.at.least', 3);
// URL
cy.url().should('include', '/dashboard');
cy.url().should('eq', 'http://localhost:3000/dashboard');
cy.location('pathname').should('eq', '/dashboard');
cy.hash().should('eq', '#section-1');// ── Explicit Assertions (expect) ──
import { expect } from 'chai';
// Use expect() for non-Cypress objects
const user = { name: 'Alice', age: 30 };
expect(user.name).to.eq('Alice');
expect(user.age).to.be.greaterThan(25);
// With DOM elements
cy.get('.card').then(($card) => {
expect($card).to.have.class('active');
expect($card.attr('data-id')).to.eq('1');
});
// Callback form for dynamic values
cy.get('.counter').should(($el) => {
const text = parseInt($el.text(), 10);
expect(text).to.be.at.least(0);
expect(text).to.be.at.most(100);
});
// ── Chaining Assertions ──
cy.get('.user-card')
.should('be.visible')
.and('have.class', 'active')
.and('contain', 'Alice');
// ── Retry-ability ──
// .should() automatically retries until assertion passes
// or the defaultCommandTimeout is reached
cy.get('.loading').should('not.exist'); // waits for loading to disappear
cy.get('.api-response').should('contain', 'success'); // waits for API
// ── Custom assertions with chai
chai.config.includeStack = false;
// Wait for specific number of elements
cy.get('.list-item').should(($items) => {
expect($items).to.have.length(3);
});| Assertion | Passes When |
|---|---|
| be.visible | Element has display, not hidden |
| be.hidden | Element is hidden or removed |
| exist | Element exists in DOM |
| not.exist | Element not in DOM |
| have.length(n) | Collection has n elements |
| Assertion | Passes When |
|---|---|
| be.enabled | Not disabled attribute |
| be.disabled | Has disabled attribute |
| be.checked | Checkbox/radio checked |
| be.focused | Element has focus |
| have.value(x) | Input value equals x |
| have.attr(k, v) | Has attribute with value |
.should() assertions are automatically retried until they pass or timeout. This is Cypress's superpower — no explicit waits needed. Use expect() (from Chai) only for non-DOM assertions.// ── Intercepting Network Requests ──
// cy.intercept() — intercept and modify requests
// Route aliasing
cy.intercept('GET', '/api/users').as('getUsers');
cy.intercept('POST', '/api/login').as('login');
cy.intercept('DELETE', '/api/users/*').as('deleteUser');
// Wait for intercepted request
cy.get('#fetch-users-btn').click();
cy.wait('@getUsers');
// Assert on request/response
cy.wait('@getUsers').its('response.statusCode').should('eq', 200);
cy.wait('@login').its('response.body').should('have.property', 'token');
// ── Stubbing Responses ──
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
],
}).as('getUsersStub');
cy.intercept('POST', '/api/login', {
statusCode: 401,
body: { error: 'Invalid credentials' },
}).as('loginFail');
// ── Modifying Requests ──
cy.intercept('POST', '/api/users', (req) => {
req.headers['X-Custom-Header'] = 'test-value';
req.body.extra = 'added field';
req.continue((res) => {
res.send({ ...res.body, timestamp: Date.now() });
});
}).as('modifiedRequest');
// ── Dynamic Responses ──
cy.intercept('GET', '/api/users*', (req) => {
// Read query params from request
const page = req.query.page || 1;
req.reply({
statusCode: 200,
body: {
users: generateMockUsers(page),
total: 100,
page: Number(page),
},
});
}).as('paginatedUsers');
// ── Response delay (simulate slow network)
cy.intercept('GET', '/api/slow', {
statusCode: 200,
body: { data: 'slow response' },
delayMs: 2000,
}).as('slowEndpoint');// ── cy.request() — Direct API Testing ──
// GET request
cy.request('GET', '/api/users').then((response) => {
expect(response.status).to.eq(200);
expect(response.body).to.be.an('array');
});
// Full request object
cy.request({
method: 'POST',
url: '/api/users',
headers: {
'Authorization': 'Bearer token123',
'Content-Type': 'application/json',
},
body: { name: 'Alice', email: 'alice@test.com' },
}).then((response) => {
expect(response.status).to.eq(201);
expect(response.body.name).to.eq('Alice');
});
// DELETE request
cy.request('DELETE', '/api/users/1').its('status').should('eq', 204);
// With auth
cy.request({
method: 'GET',
url: '/api/admin/stats',
auth: {
username: 'admin',
password: 'admin123',
},
});
// ── Intercept Aliases for Assertions ──
cy.intercept('GET', '/api/users/**').as('getAllUsers');
cy.visit('/users');
cy.wait('@getAllUsers').then(({ request, response }) => {
expect(request.headers).to.have.property('authorization');
expect(response.statusCode).to.eq(200);
expect(response.body).to.have.length(10);
});
// Wait with timeout override
cy.wait('@getUsers', { timeout: 10000 });
cy.wait('@getUsers', { responseTimeout: 15000 });| Option | Description |
|---|---|
| url / method | URL pattern and HTTP method |
| as(alias) | Name the route for cy.wait() |
| body / statusCode | Static stub response |
| delayMs | Simulate network latency |
| middleware | Run as middleware (fn) |
| times(n) | Stub only N times |
| statusCode | Stub status code |
| headers | Stub response headers |
| Chain | Accesses |
|---|---|
| .its("response.statusCode") | HTTP status code |
| .its("response.body") | Response body |
| .its("response.headers") | Response headers |
| .its("request.url") | Request URL |
| .its("request.headers") | Request headers |
| .its("request.body") | Request body |
.as('myRoute') and use cy.wait('@myRoute') to assert on them. This ensures your tests wait for network responses before asserting.{
"admin": {
"id": 1,
"name": "Alice Admin",
"email": "admin@test.com",
"role": "admin",
"password": "admin123"
},
"user": {
"id": 2,
"name": "Bob User",
"email": "user@test.com",
"role": "user",
"password": "user123"
},
"products": [
{ "id": 1, "name": "Widget", "price": 9.99 },
{ "id": 2, "name": "Gadget", "price": 24.99 },
{ "id": 3, "name": "Doohickey", "price": 14.99 }
]
}// ── Using cy.fixture() ──
// Basic fixture loading
cy.fixture('users.json').then((users) => {
cy.get('#name').type(users.admin.name);
});
// Alias for reuse
beforeEach(() => {
cy.fixture('users.json').as('users');
cy.fixture('products.json').as('products');
});
it('uses aliased fixtures', function () {
// NOTE: must use function() syntax, not arrow function
cy.log(this.users.admin.name);
cy.log(this.products[0].name);
});
// Inline fixture usage
cy.fixture('users.json').then((users) => {
cy.intercept('GET', '/api/users', users).as('getUsers');
});
// ── Dynamic Fixtures ──
// Use cy.fixture() with a callback to modify data
cy.fixture('users.json').then((users) => {
const customUser = { ...users.admin, name: 'Custom Name' };
cy.intercept('GET', '/api/current-user', customUser).as('getCurrentUser');
});
// ── Fixture with env-specific data ──
// cypress/fixtures/development/config.json
// cypress/fixtures/production/config.json
cy.fixture(Cypress.env('envName') + '/config.json').then((config) => {
cy.log(config.apiUrl);
});
// ── cy.writeFile() — Create temporary fixture files ──
cy.writeFile('cypress/fixtures/temp-data.json', {
users: [{ id: 1, name: 'Temp User' }]
});
cy.fixture('temp-data.json').then((data) => {
expect(data.users).to.have.length(1);
});| Pattern | Use Case |
|---|---|
| cy.fixture("file.json") | Load JSON fixture |
| .then((data) => ...) | Access fixture data |
| cy.as("alias") + this.alias | Reuse in tests |
| cy.fixture("file.ext", { encoding }) | Custom encoding |
| cy.writeFile("f.json", data) | Write temp data |
| cy.readFile("f.json") | Read file from disk |
| Tip | Why |
|---|---|
| Use function() not () => | this binding for aliases |
| Keep fixtures small | Faster loading, clearer data |
| One fixture per entity | users.json, products.json |
| Use cy.writeFile() | Generate dynamic test data |
| Use Cypress.env() | Environment-specific configs |
function() (not arrow functions) for test callbacks when accessing aliased fixtures via this.alias. Arrow functions don't bind this.// ── cypress/support/commands.js ──
// Login by UI
Cypress.Commands.add('login', (email, password) => {
cy.session([email, password], () => {
cy.visit('/login');
cy.get('[data-cy="email"]').type(email);
cy.get('[data-cy="password"]').type(password);
cy.get('[data-cy="submit"]').click();
cy.url().should('include', '/dashboard');
});
});
// Login via API (faster)
Cypress.Commands.add('loginByApi', (email, password) => {
cy.request('POST', '/api/auth/login', { email, password })
.then((response) => {
window.localStorage.setItem('token', response.body.token);
window.localStorage.setItem('user', JSON.stringify(response.body.user));
});
});
// Select from dropdown (custom implementation)
Cypress.Commands.add('selectDropdown', (selector, value) => {
cy.get(selector).click();
cy.get('[role="option"]').contains(value).click();
cy.get(selector).should('have.value', value);
});
// Wait for API and assert
Cypress.Commands.add('waitForApi', (method, url, alias) => {
cy.intercept(method, url).as(alias);
cy.wait('@' + alias).its('response.statusCode').should('eq', 200);
});
// Custom assertion: should have data-test-id
Cypress.Commands.add('haveTestId', (testId) => {
cy.get(`[data-testid="${testId}"]`);
});
// Drag and drop custom command
Cypress.Commands.add('drag', { prevSubject: 'element' }, (subject, target) => {
cy.wrap(subject).trigger('mousedown', { which: 1 });
cy.get(target).trigger('mousemove').trigger('mouseup');
});
// Table assertion helper
Cypress.Commands.add('tableHasRows', (rows) => {
cy.get('table tbody tr').should('have.length', rows);
});
// ── Overwrite existing commands ──
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
// Add auth header to all visits
const token = window.localStorage.getItem('token');
if (token) {
options = { ...options, headers: { Authorization: `Bearer ${token}` } };
}
return originalFn(url, options);
});// ── cypress/support/e2e.js ──
// This file runs before all spec files
// Import commands
import './commands';
// Import custom assertions
import '@testing-library/cypress/add-commands';
// ── Global Hooks ──
beforeEach(() => {
// Set default viewport
cy.viewport(1280, 720);
});
// ── Custom Assertions ──
chai.Assertion.addMethod('beAccessible', function () {
const subject = this._obj;
// Run axe accessibility check (requires @cypress/axe)
cy.wrap(subject).then(($el) => {
cy.checkA11y($el);
});
});
// ── Uncaught Exception Handling ──
Cypress.on('uncaught:exception', (err, runnable) => {
// Return false to prevent test failure
if (err.message.includes('ResizeObserver')) {
return false;
}
});| Pattern | Syntax |
|---|---|
| Basic | Cypress.Commands.add("name", fn) |
| With args | .add("login", (email, pass) => {...}) |
| Chained | .add("drag", { prevSubject: "element" }, fn) |
| Dual | .add("cmd", { prevSubject: "optional" }, fn) |
| Overwrite | Cypress.Commands.overwrite("visit", fn) |
| Plugin | Purpose |
|---|---|
| @cypress/axe | Accessibility testing |
| @testing-library/cypress | Testing Library queries |
| cypress-file-upload | File upload support |
| cypress-recurse | Retry logic patterns |
| cypress-wait-until | Custom wait conditions |
| cypress-localstorage-commands | localStorage helpers |
| cypress-plugin-tab | Tab key navigation |
| @4tw/cypress-drag-drop | Drag and drop |
cy.session() for login commands — it caches the login session and skips re-authentication on subsequent calls, making tests significantly faster.// ── Page Object Model (POM) Pattern ──
// cypress/pages/LoginPage.js
class LoginPage {
visit() {
cy.visit('/login');
return this;
}
getEmailInput() {
return cy.get('[data-cy="email"]');
}
getPasswordInput() {
return cy.get('[data-cy="password"]');
}
fillEmail(email) {
this.getEmailInput().clear().type(email);
return this;
}
fillPassword(password) {
this.getPasswordInput().clear().type(password);
return this;
}
submit() {
cy.get('[data-cy="submit"]').click();
return this;
}
login(email, password) {
this.fillEmail(email).fillPassword(password).submit();
}
getErrorMessage() {
return cy.get('[data-cy="error"]');
}
}
export default LoginPage;// ── Test with Page Object Model ──
import LoginPage from '../pages/LoginPage';
describe('Login (POM)', () => {
const loginPage = new LoginPage();
beforeEach(() => {
loginPage.visit();
});
it('should login successfully', () => {
loginPage.login('alice@test.com', 'password123');
cy.url().should('include', '/dashboard');
});
it('should show error for wrong password', () => {
loginPage.login('alice@test.com', 'wrong');
loginPage.getErrorMessage()
.should('be.visible')
.should('contain', 'Invalid');
});
it('should validate empty email', () => {
loginPage.submit();
loginPage.getErrorMessage()
.should('contain', 'Email is required');
});
});
// ── Best Practices Summary ──
// 1. Use data-cy attributes for selectors
// 2. Use POM for complex pages
// 3. Use cy.session() for auth state
// 4. Use cy.intercept() to stub APIs (fast, deterministic)
// 5. Don't test third-party libraries
// 6. Keep tests independent (no test ordering)
// 7. Use beforeEach for setup, not shared state
// 8. Use custom commands for reusable actions
// 9. Set realistic timeouts in config
// 10. Use visual testing (cypress-image-snapshot) for UI regression| Benefit | Description |
|---|---|
| Maintainability | Change locators in one place |
| Reusability | Share page actions across tests |
| Readability | Tests read like business actions |
| Encapsulation | Hide implementation details |
| Type safety | Works with TypeScript classes |
| Anti-Pattern | Instead |
|---|---|
| Hardcoded waits | Use .should() auto-retry |
| Testing 3rd-party libs | Test your own code |
| Dependent tests | Each test should be independent |
| Deep CSS selectors | Use data-cy attributes |
| Testing implementation | Test behavior, not code |
| cy.wait(5000) | Wait for state, not time |
| Multiple assertions in one chain | Break into steps |
cy.wait(number). Always wait for state or conditions: cy.get('.loading').should('not.exist') or cy.wait('@apiAlias'). Time-based waits cause flaky tests.