JavaScript · Cypress
Deep dives with Cypress
9 topics that separate a script from a suite — each with a complete example that runs against this site as is.
Cypress runs inside the browser, so it only supports JavaScript and TypeScript — there is no Python, Java or C# version of Cypress. For those languages, see Selenium or Playwright.
Deep dive 1Beginner
Locators that survive a redesign
Cypress finds elements with CSS and with visible text. cy.contains and within() let you pick one card out of many and work inside it, instead of writing long selector chains.
- ✓cy.contains(selector, text) finds the element that holds the text — the way a user spots it
- ✓within() scopes every command inside it to that element
- ✓filter(':has(…)') keeps only the elements that contain something
// cy.getByTestId is the custom command from the Cypress setup (cypress/support/commands.js).
describe('Locators that survive a redesign', () => {
it('finds a product the way a shopper does', () => {
cy.visit('/shop');
cy.get('#username').type('demo_user'); // id
cy.get('[name="password"]').type('Demo@123'); // attribute
cy.contains('button', 'Sign in').click(); // tag + visible text
// One card out of twelve, found by its text — then act inside it
cy.contains('[data-testid="product-card"]', 'Lumen Desk Lamp').within(() => {
cy.getByTestId('product-price').should('have.text', '$34.99');
cy.contains('button', 'Add to cart').click();
cy.contains('button', 'Remove').should('be.visible');
});
// Filter by what a card contains: every card that carries a "Sale" badge
cy.getByTestId('product-card')
.filter(':has([data-testid="product-badge"]:contains("Sale"))')
.should('have.length', 2);
});
});Deep dive 2Beginner
Retry-ability, never cy.wait(ms)
Cypress retries a query and its assertion together until they pass. If you let it, you never need a fixed pause — and a test that waits for the result is as fast as the page.
- ✓Queries and should() retry as one until the assertion passes or the timeout ends
- ✓slow_user takes about three seconds to sign in — raise the timeout on that one command, don't sleep
- ✓A should() callback retries a check you compute yourself, such as “are these prices sorted?”
describe('Retry-ability', () => {
it('slow_user: wait for the result, never for time', () => {
cy.visit('/shop');
cy.getByTestId('username').type('slow_user');
cy.getByTestId('password').type('Demo@123');
cy.getByTestId('login-button').click();
cy.getByTestId('login-spinner').should('be.visible');
// Retries until the slow login finishes — a longer timeout, not cy.wait(3000)
cy.location('pathname', { timeout: 10000 }).should('eq', '/shop/products');
cy.getByTestId('product-card').should('have.length', 12);
});
it('retries a check you write yourself', () => {
cy.visit('/shop');
cy.getByTestId('username').type('demo_user');
cy.getByTestId('password').type('Demo@123');
cy.getByTestId('login-button').click();
cy.getByTestId('sort-select').select('price-asc');
// A should() callback is retried as a whole until every expect inside it passes
cy.getByTestId('product-price').should(($prices) => {
const prices = [...$prices].map((el) => Number(el.innerText.replace('$', '')));
expect(prices).to.have.length(12);
expect(prices).to.deep.equal([...prices].sort((a, b) => a - b));
});
});
});Deep dive 3Intermediate
Custom commands: set-up you can name
A custom command is a step with a name. Specs say cy.loginToShop() and stay short, and when the login form changes you fix one place.
- ✓Cypress.Commands.add puts your command next to cy.get and cy.visit
- ✓A command can take arguments with defaults — the user to sign in as
- ✓No tear-down is needed: Cypress clears cookies and localStorage before every test
// cypress/support/commands.js
Cypress.Commands.add('getByTestId', (id, options) => cy.get(`[data-testid="${id}"]`, options));
// Set-up you can name: every spec can start "signed in to Nova Shop" in one line
Cypress.Commands.add('loginToShop', (user = 'demo_user') => {
cy.visit('/shop');
cy.getByTestId('username').type(user);
cy.getByTestId('password').type('Demo@123');
cy.getByTestId('login-button').click();
cy.location('pathname').should('match', /\/shop\/(products|admin)$/);
});describe('Custom commands', () => {
beforeEach(() => {
cy.loginToShop();
});
// No tear-down needed: Cypress clears cookies and localStorage before every test.
it('the cart badge counts items', () => {
cy.getByTestId('add-to-cart-p1').click();
cy.getByTestId('add-to-cart-p4').click();
cy.getByTestId('cart-badge').should('have.text', '2');
});
it('a sold-out product cannot be added', () => {
cy.getByTestId('sold-out-p3').should('be.disabled');
});
});Deep dive 4Intermediate
Sign in once with cy.session
Signing in through the UI before every test is slow and tests the login form a hundred times. cy.session runs the login once, saves the browser state, and restores it for every later test.
- ✓cy.session caches cookies, localStorage and sessionStorage under the id you give it
- ✓validate() checks the saved session is still good; if it is not, the login runs again
- ✓cy.session leaves a blank page, so visit a page right after it
// cypress/support/commands.js
Cypress.Commands.add('getByTestId', (id, options) => cy.get(`[data-testid="${id}"]`, options));
// Signs in through the UI the first time; afterwards cy.session restores the saved
// cookies and localStorage instead of running the steps again.
Cypress.Commands.add('signInOnce', (user = 'demo_user') => {
cy.session(user, () => {
cy.visit('/shop');
cy.getByTestId('username').type(user);
cy.getByTestId('password').type('Demo@123');
cy.getByTestId('login-button').click();
cy.location('pathname').should('eq', '/shop/products');
}, {
// Nova Shop keeps its session in localStorage — if it is gone, sign in again
validate() {
cy.window().then((win) => expect(win.localStorage.getItem('pa-shop')).to.contain(user));
},
});
});describe('Sign in once, reuse it everywhere', () => {
beforeEach(() => {
cy.signInOnce(); // UI login runs for the first test only
cy.visit('/shop/products'); // cy.session leaves a blank page: always visit after it
});
it('starts on the products page, already signed in', () => {
cy.getByTestId('user-greeting').should('contain', 'demo_user');
cy.getByTestId('product-card').should('have.length', 12);
});
it('is still signed in for the next test', () => {
cy.getByTestId('add-to-cart-p1').click();
cy.getByTestId('cart-badge').should('have.text', '1');
});
});Deep dive 5Intermediate
Control the network
Empty lists, server errors and odd data are hard to set up on a real server and easy to fake. cy.intercept lets you answer a request yourself, change a real response, or just watch it go by.
- ✓cy.intercept with a response stubs the request; without one it only spies
- ✓times: 1 stubs the first request only, so a retry reaches the real server
- ✓Give the intercept an alias and cy.wait('@alias') until that request has happened
describe('Control the network', () => {
it('empty list shows the empty state', () => {
cy.intercept('GET', '**/mock/products.json*', { body: [] }).as('products');
cy.visit('/dynamic/network-mocking');
cy.wait('@products');
cy.getByTestId('net-empty').should('have.text', 'No products found.');
cy.getByTestId('net-item').should('not.exist');
});
it('a server error can be retried', () => {
// times: 1 — only the first request gets the stub; the retry reaches the real server
cy.intercept({ method: 'GET', url: '**/mock/products.json*', times: 1 }, { statusCode: 500, body: 'Internal Server Error' }).as('failed');
cy.visit('/dynamic/network-mocking');
cy.wait('@failed');
cy.getByTestId('net-app').find('[role="alert"]').should('contain', '(500)');
cy.contains('button', 'Try again').click();
cy.getByTestId('net-item').should('have.length', 8);
});
});Deep dive 6Advanced
Check the UI against the API
A UI test proves the screen shows a number. A hybrid test proves it is the right number. cy.request calls an API from the same test, with the same base URL.
- ✓cy.request sends JSON and yields the status and body — no page involved
- ✓Nova Shop's quote API uses the same pricing rules as the cart screen
- ✓failOnStatusCode: false lets you assert on a 4xx instead of failing on it
const usd = (n) => '$' + n.toFixed(2);
describe('Check the UI against the API', () => {
it('cart totals in the UI match the quote API', () => {
cy.visit('/shop');
cy.getByTestId('username').type('demo_user');
cy.getByTestId('password').type('Demo@123');
cy.getByTestId('login-button').click();
cy.getByTestId('add-to-cart-p1').click();
cy.getByTestId('add-to-cart-p4').click();
cy.getByTestId('cart-link').click();
cy.getByTestId('coupon-input').type('SAVE10');
cy.getByTestId('apply-coupon').click();
// cy.request calls the API with the same base URL — no browser page involved
cy.request('POST', '/api/shop/quote', {
items: [{ id: 'p1', qty: 1 }, { id: 'p4', qty: 1 }],
coupon: 'SAVE10',
}).then(({ status, body: quote }) => {
expect(status).to.eq(200);
cy.getByTestId('subtotal').should('have.text', usd(quote.subtotal));
cy.getByTestId('discount').should('have.text', '−' + usd(quote.discount));
cy.getByTestId('tax').should('have.text', usd(quote.tax));
cy.getByTestId('total').should('have.text', usd(quote.total));
});
});
it('an unknown coupon is rejected by the API too', () => {
cy.request({
method: 'POST',
url: '/api/shop/quote',
body: { items: [{ id: 'p1', qty: 1 }], coupon: 'ABC123' },
failOnStatusCode: false, // a 4xx is the expected result here
}).then(({ status, body }) => {
expect(status).to.eq(422);
expect(body.error).to.contain('not a valid coupon');
});
});
});Deep dive 7Advanced
Two roles in one test
Real workflows cross users: a customer orders, a manager ships. One test can follow the whole journey if a small helper switches user and the order id is carried from step to step.
- ✓cy.log names each phase in the command log, so a failure says which part broke
- ✓Read the order id with .then() and use it inside the callback — Cypress values arrive later
- ✓The second role's change must be visible to the first role afterwards
const signIn = (user) => {
cy.visit('/shop');
cy.getByTestId('username').type(user);
cy.getByTestId('password').type('Demo@123');
cy.getByTestId('login-button').click();
cy.location('pathname').should('match', /\/shop\/(products|admin)$/); // signed in before the test moves on
};
describe('Two roles in one test', () => {
it('customer orders, manager ships, customer sees Shipped', () => {
cy.log('**customer places an order**');
signIn('demo_user');
cy.getByTestId('add-to-cart-p4').click();
cy.getByTestId('cart-link').click();
cy.getByTestId('checkout-button').click();
cy.getByTestId('first-name').type('Asha');
cy.getByTestId('last-name').type('Rao');
cy.getByTestId('email').type('asha@example.com');
cy.getByTestId('phone').type('5551234567');
cy.getByTestId('address').type('12 Lake Road');
cy.getByTestId('city').type('Austin');
cy.getByTestId('state').select('Texas');
cy.getByTestId('zip').type('73301');
cy.getByTestId('payment-cod').check();
cy.getByTestId('accept-terms').check();
cy.getByTestId('place-order').click();
// Read the generated order id from the page and carry it through the rest of the journey
cy.getByTestId('order-id').invoke('text').then((orderId) => {
cy.getByTestId('logout-button').click();
cy.log('**store manager ships it**');
signIn('admin_user');
cy.location('pathname').should('eq', '/shop/admin');
cy.contains('[data-testid="admin-order-row"]', orderId).within(() => {
cy.getByTestId('admin-order-customer').should('have.text', 'demo_user');
cy.contains('button', 'Mark shipped').click();
cy.getByTestId('admin-order-status').should('have.text', 'Shipped');
});
cy.getByTestId('logout-button').click();
cy.log('**customer sees the new status**');
signIn('demo_user');
cy.visit('/shop/orders');
cy.contains('[data-testid="order-row"]', orderId)
.find('[data-testid="order-status"]')
.should('have.text', 'Shipped');
});
});
it('a customer cannot open the admin page', () => {
signIn('demo_user');
cy.location('pathname').should('eq', '/shop/products');
cy.visit('/shop/admin');
cy.getByTestId('access-denied').should('be.visible');
});
});Deep dive 8Advanced
Control time
Session timeouts, hold timers and countdowns make tests slow or flaky if you wait for them. cy.clock takes over the browser's time and cy.tick moves it forward, so a ten-minute timeout takes milliseconds.
- ✓Call cy.clock before cy.visit so the page starts with your clock
- ✓Faking only Date keeps the app's own timers running, so it notices the jump by itself
- ✓TrustBank signs you out after ten idle minutes — a perfect target
describe('Control time', () => {
it('the session expires after ten idle minutes', () => {
// Fake only Date, so the app's own timers keep ticking and notice the jump
cy.clock(Date.now(), ['Date']);
cy.visit('/apps/bank');
cy.getByTestId('customer-id').type('100200300');
cy.getByTestId('bank-password').type('Bank@123');
cy.getByTestId('login-submit').click();
cy.getByTestId('security-answer').type('greenwood');
cy.getByTestId('verify-answer').click();
cy.getByTestId('customer-name').should('have.text', 'Amir Khan');
cy.tick(10 * 60 * 1000 + 1000); // ten minutes and a second, instantly
cy.getByTestId('session-expired').should('contain', 'Your session expired');
cy.getByTestId('bank-login-form').should('be.visible'); // back on the sign-in page
});
});Deep dive 9Intermediate
A config you can debug with
When a test fails in CI you can't watch it. Retries, screenshots and videos turn “it failed” into a recording you can open.
- ✓retries.runMode retries in CI only, so a flaky test shows up while you are writing it
- ✓A failed test leaves a screenshot; video records the whole spec
- ✓The viewport is a setting — run the same specs at a phone size from the command line
const { defineConfig } = require('cypress');
module.exports = defineConfig({
retries: { runMode: 2, openMode: 0 }, // retry in CI, fail fast while you write tests
video: true, // a recording of every spec run
screenshotOnRunFailure: true, // saved to cypress/screenshots
viewportWidth: 1280,
viewportHeight: 800,
e2e: {
baseUrl: process.env.BASE_URL || 'https://www.practiceqaautomation.com',
specPattern: 'cypress/e2e/**/*.cy.js',
supportFile: 'cypress/support/e2e.js',
},
});// Run on a phone viewport too: npx cypress run --config viewportWidth=412,viewportHeight=915
// Failed runs leave a screenshot and a video you can open — no need to re-run to see what happened.
describe('Smoke', () => {
it('the home page loads', () => {
cy.visit('/');
cy.title().should('contain', 'PracticeQAAutomation');
});
});