This content originally appeared on DEV Community and was authored by mohamed Said Ibrahim
How I Built a Robust Cypress.io
“Behind every successful automation project, there’s a chaotic first commit.”
How I Built a Robust Cypress.io Automation Project from Scratch — A Real-World Engineer’s Journey to Excellence
“Behind every successful automation project, there’s a chaotic first commit.”
Introduction
I remember it like it was yesterday. Our mobile-first web application was growing rapidly, and manual testing became a bottleneck. Regression bugs haunted us in every release, and the team was crying out for help. I knew it was time to act.
I decided to build a robust, scalable, and professional Cypress.io automation testing project — something maintainable, extendable, and powerful enough to cover everything from login flows to end-to-end business scenarios. What started as a simple plan turned into a real-world masterclass in Cypress, custom commands, POM, and solving unexpected errors.
This article is your map to follow that journey and build your own Cypress automation project like a pro.
Cypress
Step 1: Initialize the Project with NPM
First things first, I opened the terminal and ran:
mkdir cypress-floward-e2e
cd cypress-floward-e2e
npm init -y
This created the backbone of my project — the package.json
file.
Step 2: Install Cypress
Since Cypress is web-focused and we had some hybrid mobile flows in WebView, I started with the essential web testing stack:
npm install cypress --save-dev
Step 3: Install Required Dependencies
Next, I installed helpful plugins to supercharge my Cypress tests:
npm install \--save-dev cypress-file-upload
npm install \--save-dev cypress-axe
npm install \--save-dev cypress-xpath
These plugins enabled me to:
- Handle file uploads easily.
- Test accessibility.
- Use powerful XPath selectors when needed.
Step 4: Create a Clean Project Structure (POM)
To avoid the “spaghetti test code” syndrome, I structured my folders following the Page Object Model (POM):
cypress/
├── fixtures/ # Test data (JSON)
├── integration/ # Test specs
│ └── login/ # login.e2e.cy.js
├── support/
│ ├── commands.js # Custom Cypress commands
│ ├── index.js
│ └── pages/ # POM Files
│ └── LoginPage.js
├── plugins/
This structure made my tests readable, reusable, and scalable.
Step 5: Customize Cypress Configuration
In cypress.config.js
:
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'https://floward.com/en-eg/cairo',
supportFile: 'cypress/support/index.js',
specPattern: 'cypress/integration/\*\*/\*.cy.{js,jsx,ts,tsx}',
setupNodeEvents(on, config) {
// Add plugins here
},
},
viewportWidth: 1440,
viewportHeight: 900,
});
I also added environment-specific values using .env
or cypress.env.json
.
Step 6: Build Custom Commands
Inside cypress/support/commands.js
:
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login');
cy.get('\[data-cy=email\]').type(email);
cy.get('\[data-cy=password\]').type(password);
cy.get('\[data-cy=submit\]').click();
});
Now in any test, I can just call:
cy.login(‘admin@floward.com‘, ‘securePassword!’);
Step 7: Write Your First Real Test
login.e2e.cy.js:
import LoginPage from '../../support/pages/LoginPage';
describe('Login Suite', () => {
it('logs in with valid credentials', () => {
LoginPage.visit();
LoginPage.fillEmail('admin@floward.com');
LoginPage.fillPassword('securePassword');
LoginPage.submit();
cy.contains('Welcome back').should('be.visible');
});
});
LoginPage.js:
class LoginPage {
visit() {
cy.visit('/login');
}
fillEmail(email) {
cy.get('\[data-cy=email\]').type(email);
}
fillPassword(password) {
cy.get('\[data-cy=password\]').type(password);
}
submit() {
cy.get('\[data-cy=submit\]').click();
}
}
export default new LoginPage();
Step 8: Conquering Real-World Errors (with Solutions)
Cypress installation fails (Node version mismatch)
Fix:
Update Node.js to the LTS version (18.x
or 20.x
) and clear the npm cache:
npm cache clean --force
App crashes on WebView or iframe
Fix:
Use the right context-switching commands or access the iframe body:
cy.get('iframe').its('0.contentDocument.body').should('not.be.empty');
Element not found or detached
Fix:
Use should('be.visible')
or wait()
smartly and avoid hardcoded delays:
cy.get('\[data-cy=submit\]').should('be.visible').click();
Step 9: Bonus Power Moves for Real Teams
-
Use
data-cy
attributes in HTML for reliable element selection. -
Add screenshot/video reporting with
video: true
andscreenshotOnRunFailure: true
. -
Enable retries:
"retries": {
"runMode": 2,
"openMode": 0
}
-
Integrate with GitHub Actions or Jenkins CI.
Final Thoughts
Setting up Cypress wasn’t just about writing tests — it was about creating a test framework that could grow with the app. By using POM, smart selectors, and command abstraction, I reduced code duplication and improved reliability.
This journey transformed our team from firefighting bugs to confidently shipping features.
You are enaged with
- Cypress testing tutorial
- Cypress automation setup
- Cypress POM structure
- Cypress custom commands
- Cypress installation errors
- Cypress vs Appium
- Build Cypress project from scratch
Bonus: Want a GitHub Starter Repo or Video Series?
Let me know — I’ll send you the ready-made boilerplate to launch your Cypress automation today.
By Mohamed Said Ibrahim on July 5, 2025.
Exported from Medium on October 2, 2025.
This content originally appeared on DEV Community and was authored by mohamed Said Ibrahim