Introduction
This article is part of our Cypress Tutorial Series designed to help beginners master Cypress, starting from the basics. If you're new to Cypress, we recommend starting with the previous article, A Beginner's Guide to Cypress: End-to-End Testing Made Easy. It introduces Cypress, explains its benefits, and walks you through the setup process.
By the end of this series, you'll have the skills to confidently write, execute, and debug Cypress tests for modern web applications.
Why Cypress?
Modern web applications involve a lot of dynamic and user-driven interactions. Testing these interactions manually is tedious and error-prone. Cypress allows you to:
- Automate user interactions, such as form submissions, dropdown selections, and navigation.
- Validate UI elements and ensure they function as expected.
- Test edge cases and user flows to ensure the reliability of your application.
Now let's set up the project and get started.
Setting Up the Demo Project
Step 1: Create a Vite React Project
To follow along with this guide, you need a sample application to test. Create a new React project using Vite:
npm create vite@latest cypress-demo -- --template react-ts
cd cypress-demo
npm installNote: The extra--before--template is required on npm 7 and above. Without it, npm swallows the flag and Vite asks you to pick a template interactively.
Step 2: Create the Login Form Component
For this tutorial, we'll test a simple login form. Create a LoginForm component:
src/components/LoginForm.tsx:
import React, { useState } from 'react';
const LoginForm: React.FC = () => {
const [username, setUsername] = useState<string>('');
const [password, setPassword] = useState<string>('');
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
alert(`Welcome, ${username}!`);
};
return (
<form onSubmit={handleSubmit}>
<input
id="username"
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
id="password"
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
};
export default LoginForm;Then render it in your app by replacing the contents of src/App.tsx:
import LoginForm from './components/LoginForm';
function App() {
return <LoginForm />;
}
export default App;Start the development server and keep it running in a separate terminal:
npm run devThe app is now available at http://localhost:5173.
Step 3: Install Cypress
Install Cypress as a development dependency:
npm install cypress --save-devCypress ships with its own TypeScript type definitions, so there's no separate types package to install.
Step 4: Open Cypress
Initialize Cypress and open its interactive runner:
npx cypress openChoose E2E Testing when prompted. Cypress creates a cypress folder and a cypress.config.ts file in your project with default configurations.
Step 5: Configure Cypress
Set a baseUrl so you don't have to repeat the full address in every test. Update cypress.config.ts:
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:5173',
},
});To give your test files proper TypeScript support, create cypress/tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"types": ["cypress"]
},
"include": ["**/*.ts"]
}Why a separate tsconfig? Keeping Cypress types scoped to the cypress folder prevents them from clashing with your app's types, or with other test runners like Jest that define the same globals.
Now that the project is ready, let's dive into writing test cases.
Writing Cypress Test Cases
Cypress uses a BDD-style syntax with describe and it blocks:
- describe: Groups related test cases.
- it: Defines individual test cases.
Let's write a basic test suite for our LoginForm. Create cypress/e2e/login.cy.ts.
Creating Test Suites and Tests
A test suite is a collection of related tests, and each test validates specific functionality. Here’s an example:
describe('LoginForm Tests', () => {
it('should render the login form', () => {
cy.visit('/'); // Visit the app (uses baseUrl)
cy.get('form').should('be.visible'); // Assert the form is visible
});
it('should allow users to enter username and password', () => {
cy.visit('/'); // Each test starts fresh, so visit again
cy.get('#username').type('testuser'); // Type into username
cy.get('#password').type('password123'); // Type into password
cy.get('#username').should('have.value', 'testuser'); // Assert value
cy.get('#password').should('have.value', 'password123');
});
});Notice that both tests call cy.visit(). Cypress starts every test with a clean, blank page, so a test can't rely on a page loaded by the test before it. We'll see a cleaner way to handle this with beforeEach later in the article.
Let's break down the code above to understand each command and its usage.
Visiting Pages
The cy.visit() command is used to load a specific URL. Because we set baseUrl, a relative path is enough:
cy.visit('/');
cy.url().should('eq', 'http://localhost:5173/');This command ensures the target page loads successfully and is ready for testing.
Selecting Elements
Cypress provides powerful selectors to locate elements on the page. The examples in this section are general patterns you can apply to any app. Some selectors, like #submit-button, don't exist in our login form.
Using .get()
The .get() command locates elements by id, class, or other attributes:
cy.get('#submit-button').click();Using .find()
The .find() command narrows the search scope to elements inside a specific parent element:
cy.get('.form').find('input').type('Hello!');Tip:.type()works on a single element only. If your selector matches several elements, such as.find('input')in a form with two inputs, Cypress will throw an error. Use a more specific selector, or chain .first()or.eq(index).
Selecting by Text
Use cy.contains() to locate elements by their text content:
cy.contains('Login').click();This command is especially useful for buttons, links, and other text-based elements.
Checking for Partial Text
cy.contains() matches partial text, which is handy for dynamic content:
cy.contains('Welcome').should('exist');This ensures that elements containing partial text, such as greetings or messages, are present.
Assertions
Assertions validate that elements behave as expected.
Implicit Assertions
These are built into Cypress commands like .should():
cy.get('#username').should('be.visible');Explicit Assertions
Cypress bundles the Chai assertion library, so you can use expect directly for more complex checks:
cy.get('#title').then((element) => {
expect(element.text()).to.equal('Welcome Back!');
});Dropdown Selections
Test dropdowns by selecting a value and asserting the result:
cy.get('select').select('Option 1');
cy.get('select').should('have.value', 'Option 1');Cypress Test Isolation
Each Cypress test runs in isolation. Cypress resets the browser state (cookies, local storage, and the page itself) between tests.
describe('Test Isolation Example', () => {
it('should set a cookie', () => {
cy.setCookie('session', '12345');
cy.getCookie('session').should('exist');
});
it('should not retain the cookie', () => {
cy.getCookie('session').should('not.exist');
});
});Practical Example: Testing the Login Form
Combining everything, here’s a complete example:
describe('LoginForm Tests', () => {
beforeEach(() => {
cy.visit('http://localhost:5173'); // Replace with your app's URL
});
it('should render the login form', () => {
cy.get('form').should('be.visible'); // Assert the form is visible
});
it('should allow users to type in the fields', () => {
cy.get('#username').type('testuser'); // Type into username
cy.get('#password').type('password123'); // Type into password
cy.get('#username').should('have.value', 'testuser'); // Assert value
cy.get('#password').should('have.value', 'password123');
});
it('should display an alert on submit', () => {
cy.get('#username').type('testuser');
cy.get('#password').type('password123');
cy.get('button[type="submit"]').click();
// Listen for the alert
cy.on('window:alert', (text) => {
expect(text).to.contains('Welcome, testuser!');
});
});
});Why does the order matter? cy.on() only catches events that happen after it is registered. Setting it up before the click guarantees Cypress is listening when the alert fires. In the next article, you'll learn an even more reliable approach: using a stub to assert that the alert was actually called.
Conclusion
In this article, we explored Cypress's core features and syntax, including how to create test suites, select elements, and make assertions. By following along with the React-based demo project, you've gained hands-on experience with Cypress.
In the next article, we'll cover advanced features like stubs, spies, and handling network requests. Stay tuned, and happy testing!
