Basics 👇
Advanced 👇
bun add --dev cypress Equivalent: npm install --save-dev cypress
{
"scripts": {
"cypress:open": "cypress open",
"cypress:run": "cypress run"
},
"devDependencies": {
"cypress": "^15"
}
} bun run cypress:open # interactive runner
bun run cypress:run # headless CI import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents(on, config) {},
},
}); describe('Dashboard', () => {
context('when logged in', () => {
it('shows the welcome message', () => {
// ...
});
});
}); before(() => { /* once per describe */ });
beforeEach(() => { cy.viewport(1536, 960); });
afterEach(() => { /* cleanup */ });
after(() => { /* once per describe */ }); cy.visit('/'); // relative to baseUrl
cy.visit('/login', { qs: { next: '/' } });
cy.go('back');
cy.reload();
cy.url().should('include', '/dashboard'); cy.viewport(1536, 960);
cy.viewport('iphone-x'); Prefer stable selectors: data-test and data-testid (not data-cy).
cy.get('[data-testid=submit]');
cy.contains('Sign in');
cy.get('[data-test=form]').find('[data-testid=email]');
cy.get('[data-testid=modal]').within(() => {
cy.get('[data-testid=confirm]').click();
}); cy.get('[data-testid=submit]').click();
cy.get('[data-testid=email]').type('user@example.com');
cy.get('[data-testid=email]').clear();
cy.get('[data-testid=terms]').check();
cy.get('[data-testid=country]').select('Switzerland');
cy.get('[data-testid=search]').focus(); cy.get('[data-testid=banner]').should('exist');
cy.get('[data-testid=banner]').should('be.visible');
cy.get('[data-testid=title]').should('have.text', 'Home');
cy.get('[data-testid=email]').should('have.value', 'user@example.com');
cy.get('[data-testid=list]').should('contain', 'Item 1');
cy.get('[data-testid=submit]').should('be.disabled');
cy.get('[data-testid=link]').should('have.attr', 'href', '/about');
cy.get('[data-testid=card]').should('have.class', 'active');
cy.get('[data-testid=row]').should('have.length', 3);
// chain assertions
cy.get('[data-testid=link]')
.should('be.visible')
.and('have.attr', 'href', '/home'); cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [{ id: 1, name: 'Ada' }],
}).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
cy.get('[data-testid=user-row]').should('have.length', 1); cy.intercept('GET', '/api/users', { fixture: 'users.json' });
cy.fixture('users.json').then((users) => {
expect(users).to.have.length(2);
}); cy.request('GET', '/api/health').its('status').should('eq', 200);
cy.request({
method: 'POST',
url: '/api/login',
body: { email: 'user@example.com', password: 'secret' },
}).then((res) => {
expect(res.status).to.eq(200);
}); cy.session('logged-in-user', () => {
cy.request('POST', '/api/login', {
email: 'user@example.com',
password: 'secret',
});
}, {
validate() {
cy.request('/api/me').its('status').should('eq', 200);
},
});
cy.visit('/dashboard'); // cypress/support/e2e.ts
Cypress.Commands.add('login', (email: string, password: string) => {
cy.session([email, password], () => {
cy.request('POST', '/api/login', { email, password });
});
});
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
}
}
}
// in a spec
cy.login('user@example.com', 'secret');
cy.visit('/dashboard'); Wait on network aliases, not arbitrary delays. Use data-test / data-testid selectors. Avoid testing implementation details.
// prefer
cy.wait('@getUsers');
// avoid
cy.wait(5000);
cy.get('[data-testid=submit]', { timeout: 10000 }).click();
cy.get('[data-testid=hidden-menu]').click({ force: true }); // last resort