Cypress tests actually run inside the browser, not by sending commands to an external browser.
Let’s see it in action. Imagine you have a simple web page with a form.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
<h1>User Login</h1>
<input id="username" type="text" placeholder="Enter username">
<button id="submit-btn">Login</button>
<p id="greeting" style="display: none;">Welcome, User!</p>
<script>
document.getElementById('submit-btn').addEventListener('click', () => {
const username = document.getElementById('username').value;
if (username) {
document.getElementById('greeting').innerText = `Welcome, ${username}!`;
document.getElementById('greeting').style.display = 'block';
}
});
</script>
</body>
</html>
Now, here’s a Cypress test that interacts with this page:
// cypress/e2e/login.cy.js
describe('Login Form', () => {
beforeEach(() => {
// Visit the page before each test
cy.visit('./index.html');
});
it('should display a greeting after successful login', () => {
// Type into the username input
cy.get('#username').type('Alice');
// Click the login button
cy.get('#submit-btn').click();
// Assert that the greeting message is visible and contains the correct text
cy.get('#greeting').should('be.visible').and('contain', 'Welcome, Alice!');
});
});
When you run this test with npx cypress open, Cypress launches a real browser (like Chrome or Firefox) and navigates to index.html. The cy.get('#username').type('Alice') command doesn’t just send the text "Alice"; Cypress injects JavaScript into the running browser page to find the element with id="username" and programmatically set its value to "Alice". Similarly, cy.get('#submit-btn').click() triggers the actual click event on that button within the browser. The cy.get('#greeting').should('be.visible').and('contain', 'Welcome, Alice!') assertions are also executed directly in the browser, checking the DOM state.
This in-browser execution is the core of Cypress. It means Cypress has direct access to the DOM, network requests, and browser APIs, allowing it to provide real-time feedback, time-travel debugging, and reliable test execution.
The primary problem Cypress solves is flaky end-to-end testing. Traditional E2E frameworks often struggle with timing issues, element detachments, and race conditions because they operate externally, trying to guess the state of the application. Cypress, by running inside the browser, knows the exact state of the application at any given moment. It automatically waits for elements to appear, for network requests to complete, and for the application to become idle before proceeding. This built-in waiting mechanism eliminates most common sources of test flakiness.
To get started, you first install Cypress as a development dependency in your project:
npm install cypress --save-dev
# or
yarn add cypress --dev
Then, you can launch Cypress for the first time:
npx cypress open
This command will prompt you to set up your project. Cypress will create a cypress folder in your project’s root, containing subfolders like e2e (for end-to-end tests) and fixtures (for test data). It will also create a cypress.config.js (or .ts) file for configuration.
Your cypress.config.js might look something like this initially:
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: 'http://localhost:3000', // Example: if your app runs on localhost:3000
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', // Pattern for test files
},
});
The baseUrl is particularly useful if your application is running on a development server. Instead of writing cy.visit('http://localhost:3000/login') in every test, you can set baseUrl: 'http://localhost:3000' and then simply use cy.visit('/login').
The specPattern tells Cypress where to find your test files. By default, it looks for files ending in .cy.js, .cy.jsx, .cy.ts, or .cy.tsx within the cypress/e2e directory.
When you run npx cypress open, Cypress opens its Test Runner application. You’ll see a list of your spec files. Clicking on a spec file will launch a browser instance where Cypress executes your tests step-by-step, showing you the application’s state after each command. This visual feedback is crucial for understanding what your tests are doing and for debugging.
The commands like cy.get(), cy.type(), cy.click(), and cy.should() are your primary tools. cy.get() selects elements using CSS selectors, similar to jQuery. .type() simulates typing into input fields. .click() simulates a mouse click. .should() is used for assertions, checking if an element meets certain criteria (e.g., is visible, has specific text, is disabled).
One of the most powerful, yet often overlooked, aspects of Cypress is its command queue and retryability. Every command you chain together (e.g., cy.get(...).should(...)) is added to a queue. Cypress processes this queue, automatically retrying commands until they pass or a timeout is reached. This means you don’t typically need to write explicit waits; Cypress handles it for you by default. For instance, cy.get('#my-element').should('exist') will keep trying to find #my-element for a default timeout (usually 4 seconds) before failing. This retryability is key to Cypress’s stability.
After you’ve written your first few tests and are comfortable with the basics, you’ll naturally start looking into how to manage application state between tests.