Shipping a feature is one thing — knowing it still works three weeks later after five other features have been added is another. That's exactly what a Cypress automation suite is built for. It's not just a collection of tests; it's a safety net that runs automatically, covers your entire app from login to the last button on the last page, and tells you the moment something breaks. In this article, I'll walk you through how I structure a Cypress suite, what it covers, and why it's the first thing I set up on every project I take on.
the implementation
What is a Cypress Automation Suite?
A single Cypress test checks one thing — a button click, a form submission, a page load. A Cypress automation suite is the full collection of those tests, organized so they cover your entire application systematically.
Think of it like a checklist that runs itself. Every time a developer pushes new code, the suite opens a real browser, walks through your app from start to finish, and reports back: everything passed, or here's exactly what broke and where.
How the Suite is Structured
A well-built suite isn't a pile of test files. It follows a clear structure so tests are easy to find, easy to run, and easy to maintain.
Folder structure — one file per page Every page or feature in the app gets its own spec file. Nothing is mixed together.
| 1 | cypress/ |
| 2 | e2e/ |
| 3 | inventory/ |
| 4 | inventory-items.cy.js |
| 5 | inventory-reports.cy.js |
| 6 | trading/ |
| 7 | trading-journal.cy.js |
| 8 | portfolio.cy.js |
| 9 | running/ |
| 10 | activities.cy.js |
| 11 | race-goals.cy.js |
| 12 | auth/ |
| 13 | login.cy.js |
| 14 | user-settings.cy.js |
| 15 | fixtures/ |
| 16 | app-constants.json ← shared IDs, endpoints, test data |
| 17 | app-constants.yaml ← human-readable source of truth |
| 18 | support/ |
| 19 | commands.js ← reusable custom commands |
| 20 | e2e.js ← global setup |
This layout means when a bug shows up on the Portfolio page, you open portfolio.cy.js and you're exactly where you need to be.
Stable selectors — tests that survive UI changes
One of the biggest reasons Cypress suites break is fragile selectors. Targeting elements by CSS class or position means one styling update can break ten tests.
Instead, every interactive element in the app gets a dedicated id attribute:
| 1 | <button id="saveItem_inventoryPage">Save Item</button> |
| 2 | <input id="itemName_inventoryPage" placeholder="Item name" /> |
The naming convention follows camelCase_pageName — so the ID tells you exactly what the element is and which page it belongs to. Tests target these IDs directly and never break because of a design change.
What Each Test File Covers
Every spec file follows the same internal structure. This makes the test suite predictable — any developer can open any file and immediately understand what's being tested.
| 1 | describe("Inventory Items Page") { |
| 2 | before() → log in, seed test data |
| 3 | describe("Page load") { |
| 4 | ✓ shows the items list |
| 5 | ✓ shows correct column headers |
| 6 | ✓ shows empty state when no items exist |
| 7 | } |
| 8 | describe("Create item") { |
| 9 | ✓ opens the form when Add button is clicked |
| 10 | ✓ creates a new item with valid data |
| 11 | ✓ shows success message after save |
| 12 | ✓ new item appears in the list |
| 13 | ✓ shows error when name is empty |
| 14 | ✓ shows error when quantity is negative |
| 15 | } |
| 16 | describe("Edit item") { |
| 17 | ✓ opens edit form with existing data pre-filled |
| 18 | ✓ saves updated values correctly |
| 19 | ✓ shows error when saving with empty required field |
| 20 | } |
| 21 | describe("Delete item") { |
| 22 | ✓ shows confirmation dialog before deleting |
| 23 | ✓ removes item from list after confirm |
| 24 | ✓ does not delete when user cancels |
| 25 | } |
| 26 | describe("API error handling") { |
| 27 | ✓ shows error message when load fails |
| 28 | ✓ shows retry button on network failure |
| 29 | ✓ form shows error when save request fails |
| 30 | } |
| 31 | after() → clean up test data |
| 32 | } |
Every feature is covered from three angles: what works (happy path), what shouldn't work (validation), and what happens when things go wrong (error handling).
What the Suite Covers
| Area | What's tested |
|---|---|
| Page load | Data loads correctly, correct layout renders, empty state appears when there's no data |
| Forms | Required field validation, character limits, correct data saved to the database |
| Create / Edit / Delete | Full CRUD operations with success and failure scenarios |
| Navigation | Links go to the right pages, breadcrumbs work, back navigation is correct |
| Filters & Search | Filtering returns the right results, search is case-insensitive, empty search shows all results |
| Sorting | Columns sort ascending and descending correctly |
| Pagination | Correct number of items per page, next/previous navigation works |
Every API call the UI makes is verified — not just that it succeeds, but that it returns the right data and the UI displays it correctly.
| 1 | cy.intercept('GET', '/api/items').as('getItems') |
| 2 | cy.visit('/inventory') |
| 3 | cy.wait('@getItems').its('response.statusCode').should('eq', 200) |
When an API call fails, the test also verifies the UI handles it gracefully — showing an error message instead of a blank screen or a crash.
| Scenario | Tested |
|---|---|
| Logged-out user is redirected to login | ✅ |
| Login with valid credentials | ✅ |
| Login with wrong password shows error | ✅ |
| Session persists across page refresh | ✅ |
| Logout clears session and redirects | ✅ |
How Tests Are Kept Reliable
A test suite that passes today but randomly fails tomorrow is worse than no suite at all. Here's how I keep tests consistent:
Tests clean up after themselves. Any data created during a test is deleted in the after() hook. The database is always in the same state when the next test runs.
Tests don't depend on each other. Each test can run in isolation. If test #4 fails, test #5 still runs correctly — it doesn't depend on what test #4 did.
Tests always run headed (visible browser). I run every test with a real, visible browser:
npx cypress run --headed --browser chrome --spec "cypress/e2e/inventory/**"
You can watch every click and every assertion happen in real time. No hidden steps, no black-box results.
New IDs are registered immediately. Every time a new element is added to the app, its ID is added to app-constants.json before the test is written. The constants file is always the authority.
What the Coverage Report Looks Like
After every test run, three reports are updated: 1. Feature coverage report — which pages and features have tests, and which don't yet 2. Pass/fail history — when the suite last ran, how many tests passed, and what failed 3. Spec file registry — maps every spec file to the feature it covers This gives both the developer and the client a clear view of how much of the app is protected at any point in time.
| 1 | before(() => { |
| 2 | cy.task("log", "=== Starting Cypress Test Suite ==="); |
| 3 | }); |
| 4 | |
| 5 | afterEach(function () { |
| 6 | const title = this.currentTest?.title || "Unknown test"; |
| 7 | const state = this.currentTest?.state || "unknown"; |
| 8 | cy.task("log", `Test "${title}": ${state}`); |
| 9 | }); |
What You Get as a Client
When I deliver a feature, the Cypress suite comes with it — not as an afterthought, but as part of the definition of done. A ticket isn't closed until tests are written, run, and 100% passing.
You get:
The goal isn't just covering lines of code. It's giving you confidence that when your app is in your users' hands, it works — and when it doesn't, you find out first.
A Cypress automation suite is more than just tests — it's a structured safety net that covers every corner of your app, from page load to API errors to authentication flows. When built right, it's organized by feature, driven by stable selectors, and designed to stay green as your codebase grows. Every ticket I deliver comes with a suite that's already passing, so you ship with confidence knowing that if something breaks tomorrow, you'll be the first to know — not your users.
the testing stack
Every tool chosen with purpose — from feature to assertion.
Cypress
Modern end-to-end testing with real-time browser execution and automatic waiting
JavaScript
Used across the full stack — frontend application, test specs, and automation layer
Jenkins
runs the load tests automatically as part of the CI/CD pipeline