You know that feeling when you open a Cypress test file and see selectors scattered everywhere like confetti? Yeah, me too. It's painful.
After dealing with this mess for way too long, I created cypress-bootstrap to bring some order to the chaos. And honestly? It will change everything about how you write tests.
The Problem We All Face
Let's be real here. Most Cypress tests look like this disaster if you write as raw code:
describe('Some test', () => {
it('does something', () => {
cy.get('[data-test="username"]').type('user');
cy.get('[data-test="password"]').type('pass');
cy.get('[data-test="login-button"]').click();
cy.get('.inventory_item').should('have.length.greaterThan', 0);
cy.get('#react-burger-menu-btn').click();
cy.get('#logout_sidebar_link').click();
});
});
What happens when that username selector changes? You hunt through 50 test files to update it. Fun times, right?
The Role Model: cypress-bootstrap
This plugin brings the Page Object Model (POM) to Cypress in the cleanest way possible. No complicated setup, no fancy configurations. Just clean, organized code that actually makes sense.
Here's the same test with cypress-bootstrap way:
import LoginPage from '../../pages/LoginPage';
import InventoryPage from "../../pages/InventoryPage";
describe('Inventory Page Test Suite', () => {
it('should display products in the inventory page', () => {
InventoryPage.checkPageURL(InventoryPage.url);
InventoryPage.title().should('be.visible');
InventoryPage.inventoryItems().should('have.length.greaterThan', 0);
});
it('should add specific item to cart', () => {
InventoryPage.inventoryItem('Sauce Labs Backpack').should('be.visible');
InventoryPage.addToCartButton('Sauce Labs Backpack').click();
InventoryPage.shoppingCartButton().should('contain', '1');
});
it('should display hamburger menu functionality', () => {
InventoryPage.hamburgerMenuButton().should('be.visible').click();
InventoryPage.sideMenu.overlay().should('be.visible');
InventoryPage.sideMenu.aboutButton().should('be.visible');
InventoryPage.sideMenu.logOutButton().should('be.visible');
InventoryPage.sideMenu.closeButton().should('be.visible').click();
InventoryPage.sideMenu.overlay().should('not.be.visible');
});
});
See the difference? You can actually read what the test is doing without deciphering cryptic selectors.
The Secret Sauce: Smart Locator Design
Here's where it gets really interesting. Let's look at how the page object is structured:
import { BasePage } from '../testbase/BasePage';
class InventoryPage extends BasePage {
url = '/inventory.html';
title = () => cy.get('[data-test="title"]');
hamburgerMenuButton = () => cy.get('#react-burger-menu-btn');
filterButton = () => cy.get('.select_container');
filterSelector = () => cy.get('[data-test="product-sort-container"]');
activeFilterOption = () => cy.get('[data-test="active-option"]');
shoppingCartButton = () => cy.get('[data-test="shopping-cart-link"]');
inventoryItems = () => cy.get('[data-test="inventory-item"]');
inventoryItem = (name: string) => cy.get('[data-test="inventory-item-name"]').contains(name);
inventoryItemNameLabel = () => cy.get('[data-test="inventory-item-name"]');
inventoryItemDescription = (itemName: string) =>
cy
.get('[data-test="inventory-item-name"]')
.contains(itemName)
.closest('div[data-test="inventory-item-desc"]');
inventoryItemPrice = (itemName: string) =>
cy
.get('[data-test="inventory-item-name"]')
.contains(itemName)
.closest('div[data-test="inventory-item-price"]');
addToCartButton = (itemName: string) =>
cy
.get('[data-test="inventory-item-name"]')
.contains(itemName)
.closest('div[data-test*="add-to-cart-sauce-labs-"]');
sideMenu = {
overlay: () => cy.get('.bm-menu'),
closeButton: () => cy.get('#react-burger-cross-btn'),
allItemsButton: () => cy.get('[data-test="inventory-sidebar-link"]'),
aboutButton: () => cy.get('[data-test="about-sidebar-link"]'),
logOutButton: () => cy.get('[data-test="logout-sidebar-link"]'),
resetAppStateButton: () => cy.get('[data-test="reset-sidebar-link"]'),
};
}
export default new InventoryPage();
Notice something special here? Every locator is defined as an arrow function. This isn't just a style choice — it's actually crucial for how Cypress works.
Why Arrow Functions Are Your Best Friend
Here's the thing about Cypress: it's all about that command queue. When you define locators as arrow functions like title = () => cy.get('[data-test="title"]')
, you're not executing the command immediately. You're creating a function that returns a Cypress command when called.
This means:
- Lazy evaluation: The element lookup happens when you call the function, not when you define it
-
Fresh queries: Each time you call
InventoryPage.title()
, you get a fresh query to the DOM - No stale references: You never have to worry about elements that were found earlier but changed
Trust me, I learned this the hard way. My first attempt at this plugin used direct assignments, and it was a nightmare of stale element references.
Dynamic Locators: The Game Changer
But here's where things get really cool. Look at this locator:
inventoryItem = (name: string) => cy.get('[data-test="inventory-item-name"]').contains(name);
This is a dynamic locator. Instead of hardcoding which inventory item you want or using replace statements in the test script, you can pass the name as a parameter. So in your test, you can do:
InventoryPage.inventoryItem('Sauce Labs Backpack').should('be.visible');
InventoryPage.inventoryItem('Sauce Labs Bike Light').click();
Same locator function, different items. No more copy-pasting selectors for every single product in your inventory.
And it gets even better with complex dynamic locators:
inventoryItemPrice = (itemName: string) =>
cy
.get('[data-test="inventory-item-name"]')
.contains(itemName)
.closest('div[data-test="inventory-item-price"]');
This locator finds a specific item by name, then navigates to its price element. One function, endless possibilities.
The Singleton Pattern Magic
Here's where the architecture really shines. The plugin uses the Singleton design pattern, but not in the way you might expect.
Instead of the traditional constructor approach, your page objects extend a base class and get exported as pre-instantiated singletons:
export default new InventoryPage(); // This is the magic line
That last line? That's your singleton right there.
Why Singleton is Perfect Here
The Singleton pattern solves several problems that are unique to Cypress testing:
Memory efficiency: You get exactly one instance of each page object across your entire test suite. No matter how many times you import InventoryPage, you're always working with the same instance.
Consistent state: Since it's the same instance everywhere, you don't have weird issues with different instances having different behaviors.
Clean imports: You just import and use. No
new InventoryPage()
scattered throughout your tests.Shared functionality: All your page objects can extend BasePage and inherit common methods while maintaining their singleton nature.
Organizing Complex UI Components
The framework really shines when dealing with complex UI components. You can organize them as nested objects:
sideMenu = {
overlay: () => cy.get('.bm-menu'),
closeButton: () => cy.get('#react-burger-cross-btn'),
allItemsButton: () => cy.get('[data-test="inventory-sidebar-link"]'),
aboutButton: () => cy.get('[data-test="about-sidebar-link"]'),
logOutButton: () => cy.get('[data-test="logout-sidebar-link"]'),
resetAppStateButton: () => cy.get('[data-test="reset-sidebar-link"]'),
};
Now your tests can access complex UI elements in a logical way: InventoryPage.sideMenu.logOutButton()
or InventoryPage.sideMenu.closeButton()
.
This organization makes your tests incredibly readable. Anyone on your team can look at the test and immediately understand what's happening.
The Readability Game-Changer
Let's talk about readability because this is where the framework really pays off. Your tests become self-documenting. Look at this test:
it('should add specific item to cart', () => {
InventoryPage.inventoryItem('Sauce Labs Backpack').should('be.visible');
InventoryPage.addToCartButton('Sauce Labs Backpack').click();
InventoryPage.shoppingCartButton().should('contain', '1');
});
You don't need comments. The code tells the story: find the backpack, click its add to cart button, verify the cart shows 1 item.
Compare that to selector soup, and there's no contest.
Real-World Benefits You'll Actually Notice
Maintenance becomes painless: When a selector changes, you update it in one place. Done.
Dynamic testing gets easy: Want to test 20 different products? Use the same dynamic locator with different parameters.
New team members onboard faster: They can read and understand your tests without playing "guess the selector."
Tests become reusable: Common actions and element interactions get written once and used everywhere.
Debugging gets easier: When a test fails, you know exactly which page object method to check.
Code reviews become productive: Reviewers focus on test logic instead of deciphering what selectors do.
Getting Started Takes 2 Minutes
First, initiate a new Node.js project if you haven't already:
npm init -y
Then install the package:
npm install cypress-bootstrap
Here's the magic part — run the setup script and let it do all the heavy lifting:
# Option 1: Run the setup script directly
npx cypress-bootstrap-setup
# Option 2: Use the npm script
npm run setup
That's it! The setup script automatically creates your entire folder structure, copies all configuration files, and installs dependencies. You'll get:
├── cypress/
│ ├── pages/ # Your page objects go here
│ ├── tests/ui/ # Your test files
│ ├── testbase/ # Base classes
│ ├── testdata/ # Test data JSON files
│ ├── support/ # Cypress support files
│ └── reports/ # Test reports
├── cypress.config.ts # Main Cypress config
├── .prettierrc # Code formatting rules
└── package.json # Updated with all the scripts you need
The framework even sets up Prettier and Husky for automatic code formatting on commits. No more arguing about semicolons with your team!
NOTE: This will include some execution-ready template test cases when you install and setup. Give it a run, study them and follow the style ❤
The Bottom Line
I was tired of messy, unmaintainable tests. I came up with this framework and I saw many people writing messy Cypress tests. So I though I should share the way I work with Cypress ❤
The Singleton pattern combined with smart locator design and the Page Object Model creates a testing framework that's actually enjoyable to work with. Your tests become readable, maintainable, and reliable. Dynamic locators mean you can test complex scenarios without writing repetitive code.
Your team can onboard faster and contribute more effectively. And you spend less time hunting down broken selectors and more time building features.
Try it in your next project. I think you'll be surprised at how much cleaner your test suite becomes. And trust me, your future self will thank you when you need to test that entire product catalog without writing 50 different locators.
Highly appreciate your thoughts about this in the comment section below..
Top comments (1)
Very interesting article, and approach to selectors!
I use very often dynamic selectors and they are very useful.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.