Cypress tests don’t just check if your form submits correctly; they can and should rigorously test how your UI behaves when it’s supposed to fail.
Let’s say you have a signup form with fields for username, email, and password. We’ll use Cypress to ensure validation messages appear as expected when a user enters invalid data or leaves fields blank.
// cypress/integration/signup.spec.js
describe('Signup Form Validation', () => {
beforeEach(() => {
cy.visit('/signup'); // Assuming your signup page is at '/signup'
});
it('shows error for empty username', () => {
cy.get('[data-cy=email-input]').type('test@example.com');
cy.get('[data-cy=password-input]').type('password123');
cy.get('[data-cy=submit-button]').click();
cy.get('[data-cy=username-error]').should('be.visible').and('contain', 'Username is required');
});
it('shows error for invalid email format', () => {
cy.get('[data-cy=username-input]').type('testuser');
cy.get('[data-cy=email-input]').type('invalid-email');
cy.get('[data-cy=password-input]').type('password123');
cy.get('[data-cy=submit-button]').click();
cy.get('[data-cy=email-error]').should('be.visible').and('contain', 'Please enter a valid email address');
});
it('shows error for short password', () => {
cy.get('[data-cy=username-input]').type('testuser');
cy.get('[data-cy=email-input]').type('test@example.com');
cy.get('[data-cy=password-input]').type('pass'); // Too short
cy.get('[data-cy=submit-button]').click();
cy.get('[data-cy=password-error]').should('be.visible').and('contain', 'Password must be at least 8 characters');
});
it('shows multiple errors when multiple fields are invalid', () => {
cy.get('[data-cy=submit-button]').click(); // Submit with all fields empty
cy.get('[data-cy=username-error]').should('be.visible').and('contain', 'Username is required');
cy.get('[data-cy=email-error]').should('be.visible').and('contain', 'Email is required');
cy.get('[data-cy=password-error]').should('be.visible').and('contain', 'Password is required');
});
it('clears error message when valid input is provided after an error', () => {
cy.get('[data-cy=username-input]').type('invalid');
cy.get('[data-cy=submit-button]').click();
cy.get('[data-cy=username-error]').should('be.visible');
cy.get('[data-cy=username-input]').clear().type('validusername'); // Clear and re-type
cy.get('[data-cy=submit-button]').click();
cy.get('[data-cy=username-error]').should('not.exist'); // Error should be gone
});
});
In this example, we’re using data-cy attributes for robust element selection, which is a best practice in Cypress. The core idea is to:
- Visit the page:
cy.visit('/signup') - Interact with the form: Type invalid data or leave fields blank.
- Trigger validation: Usually by clicking a submit button (
cy.get('[data-cy=submit-button]').click()). - Assert error visibility and content: Use
.should('be.visible')and.and('contain', 'Expected error message')on the specific error elements (e.g.,[data-cy=username-error]). - Test edge cases: Empty fields, incorrect formats, minimum length violations, and combinations of errors.
- Test error clearing: Ensure errors disappear when the user corrects the input.
This comprehensive approach ensures your users get clear, immediate feedback on their input errors, leading to a much smoother and less frustrating experience.
The most surprising thing about form validation in modern front-end frameworks is how much of the "intelligence" is actually being pushed down to the browser via JavaScript, rather than relying solely on server-side checks. This allows for instant feedback before a request even hits the network, dramatically improving perceived performance and usability.
Your form’s validation logic, whether written in vanilla JavaScript, React hooks, Vue computed properties, or Angular services, is what dictates the error messages and when they appear. Cypress interacts with the DOM, observing these JavaScript-driven changes. When your validation code runs (triggered by user input or a submit attempt) and adds or removes specific classes, attributes, or content to elements associated with error states, Cypress can detect and assert against those changes.
Consider a common pattern where invalid input might trigger a class like is-invalid on the input element itself, and a sibling div with an error message might become visible. Cypress would target this:
// Example for checking an input with an error class
cy.get('[data-cy=email-input]').should('have.class', 'is-invalid');
// Example for checking a visible error message
cy.get('[data-cy=email-error]').should('be.visible');
The real power comes from chaining these assertions. You don’t just check that an error message appears; you check that it appears with the correct text, for the right field, and that it disappears when the field is corrected. This is achieved by sequencing commands and assertions. For instance, to test that an error clears upon correction, you might:
- Cause an error (e.g., type
abcinto an email field). - Assert the error message is visible.
- Clear the field (
.clear()). - Type valid data (e.g.,
test@example.com). - Assert the error message is no longer visible (
.should('not.exist')or.should('not.be.visible')).
When you write Cypress tests that interact with your form, you’re essentially simulating a user’s journey through the UI. This includes the "happy path" (successful submission) and, crucially, all the "unhappy paths" (validation failures). Each it() block represents a specific user scenario you want to guarantee. The cy.get() commands locate the elements your user would interact with or see, and .type(), .click(), .clear() simulate their actions. The .should() assertions are what verify the system’s response to those actions, confirming that the validation logic is behaving as intended by making error messages appear or disappear correctly.
The most nuanced part of testing form validation is often ensuring that only the relevant error messages are displayed. For instance, if a user has errors in both the username and email fields, you want to assert that both specific error messages are visible, and no other unrelated error messages are present. This is done by making multiple, specific assertions within a single test case, as demonstrated in the it('shows multiple errors when multiple fields are invalid', ...) example.
The next logical step after mastering basic form validation testing is to explore how to handle asynchronous validation, such as checking if a username is already taken via an API call, and ensuring the UI provides appropriate feedback during and after that process.