# API Integration Standards for Cypress
This document outlines the coding standards for API integration within Cypress tests. Following these standards will result in more maintainable, reliable, and performant tests. These standards focus on leveraging modern Cypress features and best practices.
## 1. General Principles
* **Test Real APIs:** Wherever possible, test against real backend APIs, including mocked dependencies. Stubbing should be judicious and only when absolutely necessary (e.g., third-party services with rate limits, or scenarios impossible to replicate in a test environment).
* **Isolate Tests:** Each test should be independent and not rely on the state of previous tests. This ensures test reliability and reduces debugging complexity. Prefer unique data for each test.
* **Data Setup and Teardown:** Define a clear strategy for setting up the necessary data before each test and cleaning it up afterward (e.g., database cleanup, API calls to reset state). Tools like "cy.request" and Cypress tasks can be used for this.
* **Descriptive Tests:** Ensure your test names and descriptions clearly articulate the intended behavior being tested.
* **Idempotent Tests:** Prefer idempotent API requests for setup and teardown. I.e. regardless of how many times you run them, they produce the same result (if it already exists, they do nothing).
* **Avoid Chaining "cy.request":** While "cy.request" is a powerful tool, excessive chaining can lead to less readable and maintainable code. Refactor complex API flows into custom commands.
* **Parameterize Requests:** Use variables and configuration to parameterize API requests, making it easier to change environments and API endpoints.
* **Validate Data:** Always validate the response data from API calls. Ensure the response status, headers, and body match the expected values. This builds confidence in the correctness of the system under test.
* **Error Handling:** Make sure to handle potential API errors gracefully in your tests. Verify that the application handles errors correctly and displays appropriate messages to the user.
* **Utilize "baseUrl":** Define the "baseUrl" in your "cypress.config.js" file to avoid repetitive typing of the base URL for your API.
## 2. Establishing API Interaction Patterns
This section outlines the best patterns for interacting with your backend API within Cypress.
### 2.1. Using "cy.request()" for Direct API Interaction
* **Do This:** Use "cy.request()" for making HTTP requests directly to your API. This is suitable for setting up test data, verifying background processes, or testing API endpoints directly.
* **Don't Do This:** Avoid using "cy.request()" for core user flows if possible. Focus on testing the UI's interaction with the API.
**Example:**
"""javascript
// cypress/e2e/api_integration.cy.js
describe('API Integration Tests', () => {
const apiUrl = '/api/users';
beforeEach(() => {
// Ensure a clean state before each test.
cy.request('DELETE', "${apiUrl}/cleanup");
});
it('should create a new user via API', () => {
const newUser = {
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com'
};
cy.request('POST', apiUrl, newUser).then((response) => {
expect(response.status).to.eq(201);
expect(response.body.firstName).to.eq(newUser.firstName);
expect(response.body.lastName).to.eq(newUser.lastName);
expect(response.body.email).to.eq(newUser.email);
// Optionally: Assert the 'Location' header to verify resource creation.
expect(response.headers).to.have.property('location');
});
// Optionally, verify the data in UI
cy.visit('/');
cy.contains(newUser.firstName).should('be.visible');
});
it('should get a user by ID via API', () => {
// First create a user to retrieve
cy.request('POST', apiUrl, {
firstName: 'Jane',
lastName: 'Smith',
email: 'jane.smith@example.com'
}).then((createResponse) => {
const userId = createResponse.body.id; // Assuming the API returns an 'id'
cy.request('GET', "${apiUrl}/${userId}").then((getResponse) => {
expect(getResponse.status).to.eq(200);
expect(getResponse.body.firstName).to.eq('Jane');
expect(getResponse.body.lastName).to.eq('Smith');
expect(getResponse.body.email).to.eq('jane.smith@example.com');
});
});
});
});
"""
**Explanation:**
* The "beforeEach" block ensures a clean state by cleaning up existing users.
* "cy.request()" is used to make "POST" and "GET" requests to the "/api/users" endpoint.
* The "then()" method is used to handle the response and assert the status code and response body.
* We're assuming the API returns an "id" property after a successful create operation.
* The UI is optionally checked to ensure the data has been properly updated, completing the "end-to-end" aspect of the test.
### 2.2. Custom Commands for Reusable API Interactions
* **Do This:** Create custom Cypress commands (using "Cypress.Commands.add()") for frequently used API interactions. This promotes code reuse and reduces redundancy.
* **Don't Do This:** Avoid duplicating API request logic across multiple tests.
**Example:**
"""javascript
// cypress/support/commands.js
Cypress.Commands.add('createUser', (user) => {
cy.request('POST', '/api/users', user)
.then((response) => {
expect(response.status).to.eq(201);
return response.body; // Return the created user data
});
});
Cypress.Commands.add('deleteUser', (userId) => {
cy.request('DELETE', "/api/users/${userId}")
.then((response) => {
expect(response.status).to.eq(204); // Or 200, depending on your API
});
});
// cypress/e2e/api_integration_custom_commands.cy.js
describe('API Integration with Custom Commands', () => {
let userId; // To store the ID of the created user
beforeEach(() => {
// Cleanup logic if necessary
});
it('should create and delete a user using custom commands', () => {
const newUser = {
firstName: 'Alice',
lastName: 'Wonderland',
email: 'alice@example.com'
};
cy.createUser(newUser).then((createdUser) => {
userId = createdUser.id;
expect(createdUser.firstName).to.eq(newUser.firstName);
expect(createdUser.lastName).to.eq(newUser.lastName);
expect(createdUser.email).to.eq(newUser.email);
cy.deleteUser(userId); // Deleting the user
//Verify user is deleted, using cy.request() directly.
cy.request({url:"/api/users/${userId}", failOnStatusCode: false}).then((response) => {
expect(response.status).to.eq(404); //or whichever status represents 'not found'
});
});
});
});
"""
**Explanation:**
* Custom commands "createUser" and "deleteUser" encapsulate the API request logic.
* The tests become more readable and easier to maintain by using these commands.
* "failOnStatusCode: false" option is used to check for negative API responses. This is a deliberate check for a negative case.
### 2.3. Using Cypress Tasks for Backend Operations
* **Do This:** Use Cypress tasks ("cy.task()") to offload complex backend operations to the Node.js environment. This is useful for database interactions, file system operations, or tasks not easily performed within the browser.
* **Don't Do This:** Avoid performing complex logic directly in the browser context if it can be offloaded to a task handler.
**Example:**
"""javascript
// cypress.config.js (or cypress.config.ts)
const { defineConfig } = require('cypress')
const db = require('./cypress/support/db'); // Assume a file with database functions
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
on('task', {
'db:seed': (seedData) => {
return db.seedDatabase(seedData); // Function to seed the database
},
'db:cleanup': () => {
return db.cleanupDatabase(); // Function to clean up the database
},
});
return config;
},
baseUrl: 'http://localhost:3000', // Your app's URL
},
})
"""
"""javascript
// cypress/support/db.js (Example - IMPLEMENT YOUR DATABASE FUNCTIONS HERE)
// This is a placeholder - adjust to your specific database
module.exports = {
seedDatabase: (seedData) => {
// Example using a hypothetical database library:
// db.insert(seedData).into('users');
console.log('Seeding database with:', seedData); // replace with actual DB interaction
return null; // Tasks MUST return something (or null)
},
cleanupDatabase: () => {
// Example using a hypothetical database library:
// db('users').truncate();
console.log('Cleaning up database'); // replace with actual DB interaction
return null; // Tasks MUST return something (or null)
},
};
"""
"""javascript
// cypress/e2e/api_integration_tasks.cy.js
describe('API Integration with Tasks', () => {
beforeEach(() => {
cy.task('db:cleanup'); // Clean the database before each test
cy.task('db:seed', [{ firstName: 'Initial', lastName: 'User', email: 'initial@example.com' }]); // Seed with initial data
});
it('should verify seeded data', () => {
cy.request('/api/users').then((response) => {
expect(response.status).to.eq(200);
expect(response.body).to.deep.include({ firstName: 'Initial', lastName: 'User', email: 'initial@example.com' });
});
cy.visit('/'); // Assuming your UI displays the users
cy.contains('Initial User').should('be.visible');
});
});
"""
**Explanation:**
* The "setupNodeEvents" function in "cypress.config.js" registers tasks "db:seed" and "db:cleanup". This offloads the Node.js code into a separate process.
* These tasks are called from the Cypress tests using "cy.task()".
* The database logic is kept separate from the Cypress tests, improving maintainability and security. *Important: Tasks must return a value, even if it's "null"*.
* **Important security note:** Ensure the code in your "db.js" files is properly secured and validated, as it has direct access to your database. Never expose sensitive database credentials directly in your Cypress config files.
## 3. Data Management and Fixtures
### 3.1. Using Fixtures for Static API Responses
* **Do This:** Use fixtures ("cy.fixture()") for storing static API responses or test data. This is useful for mocking API responses or providing consistent data for tests.
* **Don't Do This:** Avoid hardcoding API responses directly in your tests. Use fixtures for better organization and maintainability.
**Example:**
"""javascript
// cypress/fixtures/users.json
[
{
"id": 1,
"firstName": "Example",
"lastName": "User",
"email": "example@example.com"
}
]
"""
"""javascript
// cypress/e2e/api_integration_fixtures.cy.js
describe('API Integration with Fixtures', () => {
it('should use a fixture for API response', () => {
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers'); // Stub the API call
cy.visit('/'); // Assuming your UI makes a call to /api/users
cy.wait('@getUsers'); // Wait for the API call to be intercepted
cy.get('[data-testid="user-name"]').should('contain', 'Example User'); // Verify data shown in UI
});
it('should post data from fixtures', () => {
cy.fixture('new-user.json').then((newUser) => {
cy.request('POST', '/api/users', newUser).then((response) => {
expect(response.status).to.eq(201);
expect(response.body.firstName).to.eq(newUser.firstName);
});
});
});
});
// cypress/fixtures/new-user.json
{
"firstName": "Test",
"lastName": "User",
"email": "test@test.com"
}
"""
**Explanation:**
* The "cy.intercept()" command stubs the "/api/users" endpoint and returns the data from "users.json".
* "cy.fixture()" is used to load the fixture data.
* The test asserts that the UI displays the data from the fixture.
* Second example shows posting data from a fixture. Load the json, then use that data for a POST API call assertion.
### 3.2 Dynamic Data Generation
* **Do This:** Use libraries like "faker.js" or "chance.js" to generate dynamic data for your API requests. This ensures that your tests are not reliant on static data and are less prone to conflicts.
* **Don't Do This:** Avoid using predictable or hardcoded data for user creation or other data-driven tests, as this will lead to test contamination.
**Example:**
First install the package: "npm install @faker-js/faker --save-dev"
"""javascript
// cypress/e2e/api_integration_dynamic_data.cy.js
import { faker } from '@faker-js/faker';
describe('API Integration with Dynamic Data Generation', () => {
it('should create a user with dynamic data', () => {
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
const email = faker.internet.email({firstName: firstName, lastName: lastName}); // passing in the first and last ensures a closer match to real users
const newUser = {
firstName: firstName,
lastName: lastName,
email: email
};
cy.request('POST', '/api/users', newUser).then((response) => {
expect(response.status).to.eq(201);
expect(response.body.firstName).to.eq(firstName);
expect(response.body.lastName).to.eq(lastName);
expect(response.body.email).to.eq(email);
});
// Optional: Verify in the UI
cy.visit('/');
cy.contains("${firstName} ${lastName}").should('be.visible');
});
});
"""
**Explanation:**
* "faker" is used to generate random first names, last names, and email addresses. Using the "firstName" and "lastName" parameters to "faker.internet.email" improves the overall quality of the data.
* The test creates a new user with this dynamic data and verifies the response.
## 4. Intercepting and Stubbing API Responses
### 4.1. Intercepting API Calls with "cy.intercept()"
* **Do This:** Use "cy.intercept()" to intercept API calls and stub the responses. This is useful for simulating different scenarios, such as error conditions or slow network connections. "cy.intercept()" supports wildcards for flexible route matching.
* **Don't Do This:** Overuse stubbing. Test against real APIs whenever possible.
**Example:**
"""javascript
// cypress/e2e/api_integration_intercept.cy.js
describe('API Integration with Intercept', () => {
it('should intercept and stub an API response', () => {
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: [
{ id: 1, name: 'Stubbed Product', price: 99.99 }
]
}).as('getProducts');
cy.visit('/products'); // Assuming your UI fetches products from /api/products
cy.wait('@getProducts'); // Wait for the route to be called
cy.get('[data-testid="product-name"]').should('contain', 'Stubbed Product');
});
it('should intercept and simulate a slow API response', () => {
cy.intercept('GET', '/api/products*', (req) => {
req.reply((res) => {
res.delay(5000); // Delay the response by 5 seconds
res.send({ fixture: 'products.json' }); // Use fixture
});
}).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.get('[data-testid="product-name"]').should('be.visible');
});
it('should simulate an API error using intercept', () => {
cy.intercept('GET', '/api/products', { statusCode: 500, body: { message: 'Internal Server Error' } }).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.get('[data-testid="error-message"]').should('contain', 'Internal Server Error'); // Assertion might depend on how errors displayed in the UI
});
});
"""
**Explanation:**
* The first test stubs the response from "/api/products" with a static response.
* The second test simulates a slow API response by delaying the response for 5 seconds. This is used to test the user experience of a delayed response. Also demonstrates the use of wildcards "/api/products*" which matches any route that *starts with* "/api/products"
* The third test simulates an API error by returning a 500 status code.
### 4.2. Verifying API Request Details
* **Do This:** Use "cy.wait()" with an alias to verify the details of the intercepted API request, such as the request body, headers, or query parameters.
* **Don't Do This:** Make assumptions about the API request. Verify the actual request details to ensure the application is sending the correct data.
**Example:**
"""javascript
describe('Verifying API Request Details', () => {
it('should verify API request details', () => {
cy.intercept('POST', '/api/login').as('loginRequest');
cy.visit('/login');
cy.get('#username').type('testuser');
cy.get('#password').type('password123');
cy.get('button[type="submit"]').click();
cy.wait('@loginRequest').then((interception) => {
expect(interception.request.body).to.deep.equal({
username: 'testuser',
password: 'password123'
});
expect(interception.request.headers).to.have.property('content-type', 'application/json;charset=UTF-8');
});
});
});
"""
**Explanation:**
* The test intercepts the "POST" request to "/api/login".
* "cy.wait('@loginRequest')" waits for the request to be intercepted.
* The "then()" method is used to access the "interception" object, which contains the request and response details.
* The test asserts that the request body and headers match the expected values. Specifically this ensures that the passwords being sent are being formatted correctly for the backend.
## 5. Authentication and Authorization
### 5.1. Handling Authentication Tokens
* **Do This:** Obtain authentication tokens via API calls and store them in Cypress cookies or local storage. Use these tokens in subsequent API requests.
* **Don't Do This:** Hardcode authentication tokens in your tests. Authenticate programmatically to acquire valid tokens.
**Example:**
"""javascript
// cypress/e2e/api_integration_auth.cy.js
describe('API Integration with Authentication', () => {
it('should authenticate and make an authorized API request', () => {
cy.request('POST', '/api/login', {
username: 'testuser',
password: 'password123'
}).then((response) => {
expect(response.status).to.eq(200);
const token = response.body.token;
// Set the token in a cookie
cy.setCookie('authToken', token);
// Make an authorized API request using the cookie
cy.request({
method: 'GET',
url: '/api/protected',
headers: {
Authorization: "Bearer ${token}" // Or however your backend expects the token
},
}).then((protectedResponse) => {
expect(protectedResponse.status).to.eq(200);
expect(protectedResponse.body.message).to.eq('Authorized!');
});
});
});
it('should handle authentication through UI and store token', () => {
cy.visit('/login');
cy.get('#username').type('testuser');
cy.get('#password').type('password123');
cy.get('button[type="submit"]').click();
// Assuming successful login redirects somewhere
cy.url().should('include', '/dashboard');
//Get the Auth Token from local storage, assuming that's where it's stored on the client.
cy.window().then((window) => {
const token = window.localStorage.getItem('authToken');
expect(token).to.not.be.null;
// Now use the token for API calls
cy.request({
method: 'GET',
url: '/api/protected',
headers: {
Authorization: "Bearer ${token}"
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.message).to.eq('Authorized!');
});
});
});
});
"""
**Explanation:**
* The test first authenticates by making a "POST" request to "/api/login".
* It retrieves the authentication token from the response body.
* The token is stored as a cookie using "cy.setCookie()".
* A subsequent API request to "/api/protected" is made using the cookie.
* Also demonstrate getting the token from Local Storage after a traditional login. This is also a good demonstration on waiting for page loads before interacting with Local Storage.
### 5.2. Role-Based Access Control (RBAC)
* **Do This:** Create separate test accounts with different roles and permissions. Use these accounts to test the RBAC implementation of your API.
* **Don't Do This:** Use a single admin account for all tests. Test different roles to ensure proper access control.
**Example:**
"""javascript
// Assuming you have an API endpoint to create test users with specific roles
describe('API Integration with RBAC', () => {
it('should allow admin user to access admin-only resource', () => {
cy.request('POST', '/api/test-user', { role: 'admin' }).then((adminUserResponse) => {
const adminToken = adminUserResponse.body.token;
cy.request({
url: '/api/admin-only',
headers: { Authorization: "Bearer ${adminToken}" }
}).then((response) => {
expect(response.status).to.eq(200);
});
});
});
it('should deny regular user access to admin-only resource', () => {
cy.request('POST', '/api/test-user', { role: 'user' }).then((userResponse) => {
const userToken = userResponse.body.token;
cy.request({
url: '/api/admin-only',
failOnStatusCode: false,
headers: { Authorization: "Bearer ${userToken}" }
}).then((response) => {
expect(response.status).to.eq(403); // Assuming 403 is used for unauthorized access
});
});
});
});
"""
**Explanation:**
* The tests create test users with different roles ("admin" and "user").
* The tests verify that the admin user can access the "/api/admin-only" resource, while the regular user is denied access.
* "failOnStatusCode: false" is used to allow the test to continue even if the API returns an error status code, which is expected in this case.
## 6. Performance Testing with API Calls
### 6.1. Measuring API Response Times
* **Do This:** Use "cy.request()" to measure API response times and assert that they are within acceptable limits.
* **Don't Do This:** Ignore API performance. Ensure that your APIs are performing adequately under load.
**Example:**
"""javascript
describe('API Performance Testing', () => {
it('should verify API response time is within acceptable threshold', () => {
const maxResponseTime = 2000; // milliseconds
cy.request({
url: '/api/data',
time: true // Enable timing
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.duration).to.be.lessThan(maxResponseTime); //Asssert that it's below the maximum.
});
});
});
"""
**Explanation:**
* The "time: true" option enables timing of the API request.
* The "response.duration" property contains the response time in milliseconds.
* The test asserts that the response time is less than the specified threshold.
## 7. Environment Configuration
### 7.1. Using Environment Variables
* **Do This:** Use Cypress environment variables to configure API endpoints, authentication credentials, and other environment-specific settings.
* **Don't Do This:** Hardcode environment-specific settings in your tests. Use environment variables for better flexibility and security.
**Example:**
"""javascript
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
return config;
},
env: {
apiUrl: 'https://api.example.com', // Default API URL
username: 'testuser',
password: 'testpassword'
},
baseUrl: 'http://localhost:3000', // Your app's URL
},
})
"""
"""javascript
// cypress/e2e/api_integration_env.cy.js
describe('API Integration with Environment Variables', () => {
it('should use environment variables for API URL and credentials', () => {
const username = Cypress.env('username');
const password = Cypress.env('password');
const apiUrl = Cypress.env('apiUrl');
cy.request('POST', "${apiUrl}/api/login", {
username: username,
password: password
}).then((response) => {
expect(response.status).to.eq(200);
// ...
});
});
});
"""
**Explanation:**
* Environment variables are defined in the "cypress.config.js" file under the "env" property.
* The tests access these variables using "Cypress.env()". You can also set them using environment variables outside of the file, for example in CI.
By following these standards, you can create more robust, maintainable, and efficient Cypress tests for your API integrations. Remember to adapt these guidelines to your specific project requirements and technology stack. Regularly review and update these standards to keep them aligned with the latest Cypress features and best practices.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Security Best Practices Standards for Cypress This document outlines security best practices for writing Cypress tests. It provides specific guidelines, code examples, and explanations to help developers create secure and reliable end-to-end tests. Following these standards minimizes the risk of introducing vulnerabilities into your application during testing. ## 1. General Security Principles ### 1.1. Input Validation and Sanitization **Standard:** Always validate and sanitize any data you use in your Cypress tests, especially if it comes from the application under test or external sources. **Why:** Prevents common injection attacks, such as Cross-Site Scripting (XSS) and SQL Injection, that could be triggered by malicious data included in the application. **Do This:** * Use Cypress commands to assert against expected values instead of directly manipulating data that could be used in selectors or "cy.request" calls. * Use "cy.intercept" to control and validate request and response data, preventing the test from being affected by unexpected or malicious data from the server. **Don't Do This:** * Directly inject unsanitized data into selectors using string concatenation or template literals. * Rely on the application to sanitize data before using it in tests. **Code Example:** """javascript // Good: Validate the response from the server it('should display the correct username', () => { cy.intercept('GET', '/api/user').as('getUser'); cy.visit('/profile'); cy.wait('@getUser').then((interception) => { expect(interception.response.statusCode).to.equal(200); expect(interception.response.body.username).to.be.a('string'); cy.get('[data-testid="username"]').should('contain', interception.response.body.username); }); }); // Bad: Directly using the response to construct a selector - XSS vulnerability it('should display the correct username (INSECURE)', () => { cy.intercept('GET', '/api/user').as('getUser'); cy.visit('/profile'); cy.wait('@getUser').then((interception) => { const username = interception.response.body.username; cy.get("[data-username="${username}"]").should('be.visible'); //Potential XSS vulnerability if username contains malicious code }); }); """ ### 1.2. Principle of Least Privilege **Standard:** Run Cypress tests with the minimum required privileges. **Why:** Limits the potential damage if a test is compromised or contains a vulnerability. **Do This:** * Use dedicated test accounts with minimal permissions to interact with the application under test. * Avoid using administrator or superuser accounts in your tests. **Don't Do This:** * Run Cypress tests with highly privileged accounts. **Code Example:** """javascript // Good: Logging in with a test user that has limited access it('should allow a basic user to view their profile', () => { cy.visit('/login'); cy.get('#username').type('testuser'); cy.get('#password').type('password'); cy.get('#login').click(); cy.url().should('include', '/profile'); }); // Avoid using admin accounts in tests. """ ### 1.3. Secure Data Storage **Standard:** Avoid storing sensitive data (e.g., passwords, API keys) directly in Cypress tests or configuration files. Use environment variables for confidential information. **Why:** Protects sensitive data from being exposed in version control systems or test reports. **Do This:** * Use environment variables to store sensitive data. Retrieve these values within your Cypress tests and configurations. **Don't Do This:** * Hardcode sensitive credentials within your test files or configuration files. * Commit sensitive data to version control. **Code Example:** """javascript // cypress.config.js module.exports = { env: { username: process.env.TEST_USERNAME, password: process.env.TEST_PASSWORD } }; // Cypress test it('should login with credentials from environment variables', () => { cy.visit('/login'); cy.get('#username').type(Cypress.env('username')); cy.get('#password').type(Cypress.env('password')); cy.get('#login').click(); cy.url().should('include', '/dashboard'); }); """ ### 1.4. Clear Sensitive Data **Standard:** For tests dealing with sensitive user information or PII (Personally Identifiable Information), ensure that data is cleared or reset after the test completes. **Why:** Prevents leakage of sensitive information, even if the user is fictional or the data is for testing purposes only. Ensures compliance with privacy regulations. **Do This:** * If possible, delete the test user or data created during the test using API calls or database cleanup scripts triggered by "after" or "afterEach" hooks. **Don't Do This:** * Leave test data in the system, especially if it contains sensitive information. **Code Example:** """javascript // Cypress test describe('User profile tests', () => { let userId; beforeEach(() => { // Create a test user cy.request('POST', '/api/users', { username: 'testuser', email: 'test@example.com' }) .then((response) => { userId = response.body.id; }); }); it('should update user profile', () => { // Test code here cy.visit('/profile'); cy.get('#edit-profile').click(); cy.get('#email').clear().type('newemail@example.com'); cy.get('#save-profile').click(); cy.contains('Profile updated successfully').should('be.visible'); }); afterEach(() => { // Delete the test user after the test cy.request('DELETE', "/api/users/${userId}"); }); }); """ ## 2. Specific Cypress Security Practices ### 2.1. Preventing Cross-Site Scripting (XSS) **Standard:** Be extremely cautious when injecting data into selectors, especially data retrieved from the application. **Why:** Prevents malicious code from being injected into your application through your tests. **Do This:** * Use parameterized queries or prepared statements when interacting with databases via "cy.request". * Encode any data displayed in your application that comes from external sources. **Don't Do This:** * Directly use unsanitized data in selectors, as it can create vulnerabilities. **Code Example:** """javascript //Good: Safely using data in selectors with assertions ONLY. it('should show user in table', () => { cy.intercept('GET', '/api/user').as('getUser'); cy.visit('/users'); cy.wait('@getUser').then((interception) => { const username = interception.response.body.username; cy.get('[data-testid="user-table"]').should('contain', username); // Assertion is safe }); }); // Bad: Building a selector with potentially malicious data it('should show user in table (INSECURE)', () => { cy.intercept('GET', '/api/user').as('getUser'); cy.visit('/users'); cy.wait('@getUser').then((interception) => { const username = interception.response.body.username; cy.get("#user-${username}").should('be.visible'); // Vulnerable to injection }); }); """ ### 2.2. Avoiding SQL Injection **Standard:** When using "cy.request" to interact with APIs that might execute SQL queries, ensure the data you send is properly sanitized and validated. **Why:** Prevents attackers from manipulating database queries through your tests, leading to data breaches or unauthorized access. **Do This:** * Utilize parameterized queries when constructing your "cy.request" parameters, especially when creating test data through API calls. * If directly sending SQL through test setup (development/test environment only), always use parameterized queries. **Don't Do This:** * Directly concatenate user-provided input into SQL queries within API calls. This practice is dangerous in ANY environment, even test. **Code Example:** """javascript // Good: Using parameterized queries in the API (Backend implementation) it('should create a new user via API (Parameterized)', () => { cy.request('POST', '/api/users', { username: 'safeuser', email: 'safeuser@example.com', }).then((response) => { expect(response.status).to.eq(201); }); }); // Demonstrating parameterized queries in test setup (SQL injection prevention) - Example only, not directly Cypress code // Backend code (Node.js with a hypothetical database library) /* const createUser = (username, email) => { const query = 'INSERT INTO users (username, email) VALUES (?, ?)'; // Parameterized query db.query(query, [username, email], (err, results) => { if (err) { console.error(err); } else { console.log(results); } }); }; // Calling the function safely: createUser('safeuser', 'safeuser@example.com'); */ // Bad: Directly concatenating user input into SQL queries (SQL injection vulnerability) - NEVER DO THIS // (Illustrative example of what NOT to do, NOT Cypress code) /* // Vulnerable backend code (Node.js with a hypothetical database library) const createUser = (username, email) => { const query = "INSERT INTO users (username, email) VALUES ('${username}', '${email}')"; // SQL injection vulnerability! db.query(query, (err, results) => { if (err) { console.error(err); } else { console.log(results); } }); }; // NEVER DO THIS: createUser("'; DROP TABLE users; --", "bad@example.com"); // SQL injection attack! */ """ ### 2.3. Handling Sensitive Information in Logs and Reports **Standard:** Prevent sensitive information from being accidentally logged or included in test reports. **Why:** Reduces the risk of sensitive data exposure if reports or logs are compromised. **Do This:** * Avoid logging sensitive data in Cypress tests. Use "cy.log" with caution. Sanitize any sensitive data before logging if it is absolutely necessary. * Configure CI/CD pipelines to securely store test reports and prevent unauthorized access. **Don't Do This:** * Log sensitive data such as passwords, API keys, or PII. * Expose test reports to the public or store them in insecure locations. **Code Example:** """javascript //Good: Avoid logging sensitive data it('should login successfully (no sensitive data in logs)', () => { cy.visit('/login'); cy.get('#username').type(Cypress.env('username')); cy.get('#password').type(Cypress.env('password')); cy.get('#login').click(); cy.url().should('include', '/dashboard'); cy.log('Login successful'); // General log, no sensitive data }); // Bad: Logging sensitive data directly (AVOID!) it('should login successfully (INSECURE)', () => { cy.visit('/login'); const username = Cypress.env('username'); const password = Cypress.env('password'); cy.get('#username').type(username); cy.get('#password').type(password); cy.get('#login').click(); cy.url().should('include', '/dashboard'); cy.log("Username: ${username}, Password: ${password}"); // Sensitive data in logs - BAD! }); """ ### 2.4. Cross-Origin Resource Sharing (CORS) **Standard:** Understand and correctly configure CORS when testing applications that interact with different domains. **Why:** Prevents unexpected behavior and security risks when your Cypress tests make requests to external resources. **Do This:** * Ensure your application's CORS configuration is appropriate for your testing environment. Consider loosening CORS restrictions in test environments if necessary, but NEVER in production. * Use "cy.request" to bypass the browser's CORS restrictions when making API calls directly in your tests. This is ONLY acceptable for testing purposes. The application itself *must* have proper CORS. * Use "cy.origin" to interact with different origins in your tests, while being aware of its limitations. **Don't Do This:** * Disable CORS completely in your application's production environment to make testing easier. This defeats the entire purpose of CORS as a security feature. * Rely on browser extensions to disable CORS, as this can lead to inconsistent test results and security risks. **Code Example:** """javascript // Good: Using cy.request to bypass CORS for API testing in the test environment. it('should fetch data from a different origin via API', () => { cy.request('https://api.example.com/data') .then((response) => { expect(response.status).to.eq(200); expect(response.body).to.not.be.empty; }); }); // Good - Using cy.origin to visit a different origin - Cypress v12+ it('should visit a different origin and interact with elements', () => { cy.visit('https://www.example.com'); cy.origin('https://www.google.com', () => { cy.visit('/'); cy.get("input[name='q']").type('cypress{enter}'); cy.get('#search').should('be.visible'); }); }); // Bad: Disabling webSecurity (NOT RECOMMENDED, only for specific testing scenarios) which can be a HUGE security risk // cypress.config.js - AVOID UNLESS ABSOLUTELY NECESSARY FOR LEGACY SYSTEMS /* module.exports = { e2e: { setupNodeEvents(on, config) { // implement node event listeners here }, webSecurity: false, // Disables web security, including CORS protection. AVOID! }, }; */ """ ### 2.5. Authentication and Authorization Testing **Standard:** Thoroughly test authentication and authorization mechanisms in your application using Cypress, but protect credentials as described above. **Why:** Ensures that only authorized users can access specific resources and functionalities. **Do This:** * Use "cy.request" to programmatically log in and obtain authentication tokens before starting your tests. * Validate that unauthorized users are correctly denied access to protected resources. * Test different roles and permissions to ensure that the authorization logic is working correctly. * Test for common authentication vulnerabilities, such as brute-force attacks and session hijacking. * When using "cy.session", ensure the cache is cleared between test suites where appropriate to prevent state leakage. Consider its security implications carefully. **Don't Do This:** * Rely solely on UI-based authentication for testing your application's security. * Bypass authentication mechanisms in your tests, as this can lead to false positives. **Code Example:** """javascript // Good: Programmatically logging in and testing authorization describe('Authorization Tests', () => { let authToken; before(() => { // Programmatically log in and obtain an authentication token cy.request({ method: 'POST', url: '/api/login', body: { username: Cypress.env('username'), password: Cypress.env('password'), }, }).then((response) => { expect(response.status).to.eq(200); authToken = response.body.token; }); }); it('should allow authorized users to access protected resources', () => { cy.request({ method: 'GET', url: '/api/protected', headers: { Authorization: "Bearer ${authToken}", }, }).then((response) => { expect(response.status).to.eq(200); expect(response.body.message).to.eq('Access granted'); }); }); it('should deny unauthorized users from accessing protected resources', () => { cy.request({ method: 'GET', url: '/api/protected', failOnStatusCode: false, }).then((response) => { expect(response.status).to.eq(401); // Or appropiate error code expect(response.body.error).to.eq('Unauthorized'); }); }); }); // Good: Using cy.session - Ensure session is cleared if needed describe('User profile tests with session', () => { beforeEach(() => { cy.session('user', () => { // Unique key name cy.visit('/login'); cy.get('#username').type(Cypress.env('username')); cy.get('#password').type(Cypress.env('password')); cy.get('#login').click(); cy.url().should('include', '/dashboard'); }); cy.visit('/dashboard'); // Now dashboard is accessible via the session }); it('should display the user profile', () => { cy.get('#profile-name').should('be.visible'); }); // Optional: clear session to avoid conflicts with other test suites potentially using login. after(() => { // Uncomment the code below if you are getting "cy.session() already exists" error // localStorage.clear() // sessionStorage.clear() // cy.clearCookies() // cy.clearLocalStorage() // cy.window().then((win) => { // win.sessionStorage.clear() }); }); """ ### 2.6. Preventing Clickjacking **Standard:** Verify that your application implements defenses against clickjacking attacks. **Why:** Protects users from being tricked into performing actions they did not intend to perform. **Do This:** * Check for the presence of the "X-Frame-Options" header and ensure it is set to "DENY" or "SAMEORIGIN". * Test for the "Content-Security-Policy" header and verify that it restricts framing to trusted origins. **Don't Do This:** * Assume that your application is protected against clickjacking without proper verification. * Use deprecated methods for clickjacking protection. **Code Example:** """javascript // Checking X-Frame-Options header it('should protect against clickjacking with X-Frame-Options', () => { cy.request({ url: '/', // Replace with your application's URL failOnStatusCode: false, }).then((response) => { expect(response.headers['x-frame-options']).to.be.oneOf(['DENY', 'SAMEORIGIN']); }); }); // Checking Content-Security-Policy header for frame-ancestors it('should protect against clickjacking with Content-Security-Policy', () => { cy.request({ url: '/', // Replace with your application's URL failOnStatusCode: false, }).then((response) => { const csp = response.headers['content-security-policy']; expect(csp).to.include("frame-ancestors 'self'"); // Adjust as needed }); }); """ ### 2.7. Secure File Uploads **Standard:** If your application allows file uploads, thoroughly test the security of the upload process. **Why:** Prevents malicious files from being uploaded and executed on your server. **Do This:** * Verify that the server-side validates file types, sizes, and contents. * Test for directory traversal vulnerabilities by attempting to upload files with malicious filenames. * Ensure that uploaded files are stored in a secure location outside of the webroot. **Don't Do This:** * Rely solely on client-side validation for file uploads. * Allow users to upload executable files without proper security measures. * Store uploaded files in the same directory as your application. **Code Example:** """javascript // Good Example - Test file validation. it('should allow uploading of a valid image file', () => { cy.visit('/upload'); const filePath = 'images/valid-image.png'; // Ensure this file exists in your fixtures. cy.get('input[type="file"]').attachFile(filePath); cy.get('#upload-button').click(); cy.contains('File uploaded successfully').should('be.visible'); }); it('should reject uploading of a malicious file', () => { cy.visit('/upload'); const filePath = 'files/malicious.php'; // Attempt to upload a PHP file cy.get('input[type="file"]').then(($input) => { // Programmatically trigger the file selection event const blob = new Blob(['<?php echo shell_exec($_GET["cmd"]); ?>'], { type: 'application/x-php' }); // Malicious PHP Content const file = new File([blob], 'malicious.php', { type: 'application/x-php' }); const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); $input[0].files = dataTransfer.files; }); cy.get('#upload-button').click(); cy.contains('Invalid file type').should('be.visible'); }); """ ### 2.8. Timeout Management **Standard:** Set appropriate timeouts for Cypress commands to prevent tests from hanging indefinitely. **Why:** Prevents tests from running for excessive time, which can waste resources and hide potential performance issues. **Do This:** * Use the "timeout" option to set explicit timeouts for Cypress commands. * Adjust timeouts based on the expected response time of your application. **Don't Do This:** * Set extremely long timeouts for all commands, as this can mask performance problems. * Rely on the default Cypress timeout, which may not be appropriate for all scenarios. **Code Example:** """javascript it('should load data within a reasonable time', () => { cy.visit('/data'); cy.get('#data-table', { timeout: 10000 }).should('be.visible'); // Set an extended timeout of 10 seconds }); """ ## 3. Continuous Integration/Continuous Deployment (CI/CD) Security ### 3.1. Secure Pipeline Configuration **Standard:** Harden your CI/CD pipelines to prevent injection attacks and unauthorized access to test data. **Why:** Your CI/CD pipeline is an integral part of your SDLC. Any compromise here risks all of your processes. **Do This:** * Employ secure coding practices when defining CI/CD configurations. * Use environment variables to mask configuration data. **Don't Do This:** * Commit sensitive data directly into configuration files. ### 3.2. Vulnerability Scanning **Standard:** Regularly scan your Cypress tests and dependencies for known vulnerabilities. **Why:** Identify and address security risks in your test code and third-party libraries. **Do This:** * Integrate vulnerability scanning tools into your CI/CD pipeline. * Update your Cypress dependencies regularly to patch security vulnerabilities. **Don't Do This:** * Ignore vulnerability warnings or postpone security updates. ## 4. Ongoing Security Awareness ### 4.1 Training and Documentation **Standard:** Provide ongoing security training to Cypress developers and maintain up-to-date security documentation. **Why:** Keeps developers informed about the latest security threats and best practices. **Do This:** * Conduct regular security training sessions for your development team. * Maintain a comprehensive security guide for Cypress testing. **Don't Do This:** * Assume that developers are aware of all security best practices. * Neglect to update your security documentation as new threats emerge. ### 4.2. Code Reviews **Standard:** Implement thorough code reviews to identify potential security vulnerabilities early in the development process. **Why:** Harness collective brainpower to uncover vulnerabilities. **Do This:** * Mandate security-focused code reviews for all Cypress tests. * Use automated code analysis tools to assist with code reviews. **Don't Do This:** * Skip code reviews or rush through the process. By implementing these security best practices, you can create more secure and reliable Cypress tests, reducing the risk of introducing vulnerabilities into your application. This document should be reviewed and updated regularly to reflect the latest security threats and best practices.
# Testing Methodologies Standards for Cypress This document outlines the testing methodologies standards for Cypress, focusing on strategies for unit, integration, and end-to-end (E2E) testing. These standards aim to ensure maintainable, performant, and secure Cypress tests. ## 1. General Testing Principles ### 1.1. Test Pyramid **Standard:** Adhere to the test pyramid principles. * **Do This:** Have more unit tests, fewer integration tests, and even fewer E2E tests. * **Don't Do This:** Rely solely on E2E tests, as they are slower and more brittle. **Why:** The test pyramid helps balance test coverage with execution speed and maintainability. Excessive E2E tests can lead to slow test suites and increased maintenance costs. **Code Example (Conceptual):** """javascript // This is a conceptual example, not directly Cypress code. // Imagine a feature requiring comprehensive testing. // Unit Tests: 100+ (Focus on individual components) // Integration Tests: 20-30 (Focus on component interactions) // E2E Tests: 5-10 (Focus on critical user flows) """ ### 1.2. Test-Driven Development (TDD) **Standard:** Consider adopting TDD for new features. * **Do This:** Write failing tests *before* implementing the associated code. * **Don't Do This:** Write code first and tests after. **Why:** TDD promotes better code design, reduces the risk of untested code, and leads to higher test coverage. **Code Example (Conceptual):** 1. Write a failing unit test for a function that validates email format. 2. Implement the email validation function to pass the test. 3. Refactor the code, keeping the test passing. ### 1.3. Behavior-Driven Development (BDD) **Standard:** Use BDD principles to describe test scenarios from a user's perspective. * **Do This:** Write tests that describe the desired behavior of the application, using scenarios with "Given", "When", "Then" blocks. * **Don't Do This:** Write tests that only focus on technical implementation details. **Why:** BDD improves communication between developers, testers, and stakeholders by focusing on the "what" rather than the "how." Cypress's syntax naturally supports BDD. **Code Example:** """javascript // cypress/e2e/login.cy.js describe('Login Functionality', () => { it('allows a user to log in with valid credentials', () => { // Given (Arrange) cy.visit('/login'); // When (Act) cy.get('#username').type('valid_user'); cy.get('#password').type('valid_password'); cy.get('button[type="submit"]').click(); // Then (Assert) cy.url().should('include', '/dashboard'); cy.contains('Welcome, valid_user').should('be.visible'); }); it('displays an error message with invalid credentials', () => { // Given cy.visit('/login'); // When cy.get('#username').type('invalid_user'); cy.get('#password').type('invalid_password'); cy.get('button[type="submit"]').click(); // Then cy.contains('Invalid credentials').should('be.visible'); }); }); """ ## 2. Cypress and Testing Levels ### 2.1. Unit Testing with Cypress (Limited) **Standard:** Primarily use Cypress for integration and E2E testing, and use other tools like Mocha/Chai/Jest for unit tests. * **Do This:** If absolutely necessary, use Cypress "cy.wrap()" to test utility functions or individual components in isolation. * **Don't Do This:** Use Cypress as your primary unit testing framework. It's overkill and slower than dedicated unit testing tools. **Why:** Cypress is designed for browser-based testing, making it less efficient for pure unit testing, which should focus on isolated functions or components. **Code Example (Discouraged):** """javascript // cypress/e2e/utility.cy.js (BAD PRACTICE) //Testing a function that is not related to user flow. Using a simple "assert" is quicker describe('Utility Function Tests', () => { it('validates email format', () => { const isValidEmail = (email) => { // Simplified email validation logic return email.includes('@') && email.includes('.'); }; cy.wrap(isValidEmail('test@example.com')).should('be.true'); cy.wrap(isValidEmail('testexample.com')).should('be.false'); }); }); """ **Better alternative using Mocha's "assert":** """javascript // test/unit/utility.spec.js (using Mocha/Chai) const assert = require('chai').assert; describe('Utility Function Tests', () => { it('validates email format', () => { const isValidEmail = (email) => { return email.includes('@') && email.includes('.'); }; assert.isTrue(isValidEmail('test@example.com')); assert.isFalse(isValidEmail('testexample.com')); }); }); """ ### 2.2. Integration Testing with Cypress **Standard:** Use Cypress to test interactions between components and services. * **Do This:** Mock API calls using "cy.intercept()" to control the data flow and isolate components. * **Don't Do This:** Rely on real APIs for all integration tests, as this can lead to flaky tests and dependencies on external services. **Why:** Integration tests verify that different parts of the application work together correctly. Mocking allows you to test specific scenarios without relying on the availability or stability of external APIs. **Code Example:** """javascript // cypress/e2e/integration.cy.js describe('User List Integration', () => { beforeEach(() => { cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers'); //using fixture data cy.visit('/users'); }); it('displays a list of users from a mocked API', () => { cy.wait('@getUsers'); cy.get('.user-list-item').should('have.length', 3); // Assuming users.json has 3 users }); it('handles an error when fetching users', () => { cy.intercept('GET', '/api/users', { statusCode: 500 }).as('getUsers'); cy.visit('/users'); cy.wait('@getUsers'); cy.contains('Error fetching users').should('be.visible'); }); }); """ **Anti-pattern:** Using "cy.wait(5000)" instead of "cy.wait('@getUsers')". The explicit wait ties test execution to a specific amount of time. If the API call takes longer, tests fail, giving the false impression that the UI has an issue. ### 2.3. End-to-End (E2E) Testing with Cypress **Standard:** Use Cypress to test critical user flows from start to finish. * **Do This:** Focus on key scenarios like login, checkout, and data submission. * **Don't Do This:** Attempt to cover every possible user interaction with E2E tests. **Why:** E2E tests simulate real user behavior, verifying that the application functions correctly across all layers. However, they are more time-consuming to write and maintain. **Code Example:** """javascript // cypress/e2e/checkout.cy.js describe('Checkout Flow', () => { it('allows a user to complete the checkout process', () => { // Visit the product page cy.visit('/product/123'); // Add the product to the cart cy.get('button[data-testid="add-to-cart"]').click(); // Go to cart cy.visit('/cart'); // Proceed to checkout cy.get('button[data-testid="checkout"]').click(); // Fill in shipping information cy.get('#shipping-address').type('123 Main St'); cy.get('#city').type('Anytown'); cy.get('#zip').type('12345'); // Proceed to payment cy.get('button[data-testid="continue-to-payment"]').click(); // Fill in payment information cy.get('#credit-card').type('1111222233334444'); cy.get('#expiration-date').type('12/24'); cy.get('#cvv').type('123'); // Submit the order cy.get('button[data-testid="submit-order"]').click(); // Verify order confirmation cy.contains('Order Confirmation').should('be.visible'); cy.contains('Thank you for your order!').should('be.visible'); }); }); """ ## 3. Cypress Modern Approaches and Patterns ### 3.1. Page Object Model (POM) **Standard:** Implement the Page Object Model to abstract UI elements and interactions. * **Do This:** Create separate classes for each page or component, containing selectors and methods for interacting with elements. * **Don't Do This:** Directly embed selectors and interactions within test files. **Why:** POM improves test maintainability by centralizing UI element definitions. Changes to the UI only require updates in the page object, not in every test referencing that element. **Code Example:** """javascript // cypress/support/page_objects/LoginPage.js class LoginPage { getUsernameField() { return cy.get('#username'); } getPasswordField() { return cy.get('#password'); } getSubmitButton() { return cy.get('button[type="submit"]'); } login(username, password) { this.getUsernameField().type(username); this.getPasswordField().type(password); this.getSubmitButton().click(); } getErrorMessage() { return cy.contains('Invalid credentials'); } } export default LoginPage; """ """javascript // cypress/e2e/login.cy.js import LoginPage from '../../support/page_objects/LoginPage'; describe('Login Functionality', () => { const loginPage = new LoginPage(); //instantiate a new instance of the LoginPage obj. it('allows a user to log in with valid credentials', () => { cy.visit('/login'); loginPage.login('valid_user', 'valid_password'); cy.url().should('include', '/dashboard'); }); it('displays an error message with invalid credentials', () => { cy.visit('/login'); loginPage.login('invalid_user', 'invalid_password'); loginPage.getErrorMessage().should('be.visible'); }); }); """ ### 3.2. Custom Commands **Standard:** Create custom Cypress commands to encapsulate reusable actions. * **Do This:** Define custom commands in "cypress/support/commands.js" for common tasks like logging in, creating users, or navigating to specific pages. * **Don't Do This:** Duplicate code across multiple tests when a single custom command could handle it. **Why:** Custom commands reduce code duplication and make tests more readable. **Code Example:** """javascript // cypress/support/commands.js Cypress.Commands.add('login', (username, password) => { cy.visit('/login'); cy.get('#username').type(username); cy.get('#password').type(password); cy.get('button[type="submit"]').click(); }); """ """javascript // cypress/e2e/dashboard.cy.js describe('Dashboard', () => { it('displays the dashboard after successful login', () => { cy.login('valid_user', 'valid_password'); cy.url().should('include', '/dashboard'); }); }); """ ### 3.3. Data Fixtures **Standard:** Use data fixtures for test data. * **Do This:** Store test data in JSON files within the "cypress/fixtures" directory. * **Don't Do This:** Hardcode test data directly in test files. **Why:** Data fixtures improve test readability and maintainability. They also allow you to easily switch between different data sets. **Code Example:** """javascript // cypress/fixtures/user.json { "username": "test_user", "password": "test_password" } """ """javascript // cypress/e2e/login.cy.js describe('Login Functionality', () => { it('allows a user to login with valid credentials using fixture data', () => { cy.fixture('user').then((user) => { cy.visit('/login'); cy.get('#username').type(user.username); cy.get('#password').type(user.password); cy.get('button[type="submit"]').click(); cy.url().should('include', '/dashboard'); }); }); }); """ ### 3.4. Using "cy.session()" (Cypress 10+) **Standard:** Use "cy.session()" to cache and reuse sessions across multiple tests. * **Do This:** Define sessions for authentication or other expensive operations. * **Don't Do This:** Login before *every* test, as this significantly increases test execution time. **Why:** "cy.session()" drastically improve test performance by avoiding redundant setup steps. **Code Example:** """javascript // cypress/e2e/session.cy.js describe('Session Management', () => { beforeEach(() => { cy.session('login', () => { cy.visit('/login'); cy.get('#username').type('valid_user'); cy.get('#password').type('valid_password'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/dashboard'); }); }); it('displays the dashboard for an authenticated user', () => { cy.visit('/dashboard'); cy.contains('Welcome, valid_user').should('be.visible'); }); it('allows the user to view profile information', () => { cy.visit('/profile'); cy.contains('Profile Information').should('be.visible'); }); }); """ ### 3.5. Component Testing with Cypress **Standard:** Use modern Cypress component testing capabilities. * **Do This:** Write specific component tests in isolation to verify that each component functions correctly. Use "cy.mount()" or similar function to achieve this. * **Don't Do This:** Rely solely on end-to-end tests to verify the behavior of smaller components. **Why:** Component testing can greatly improve confidence and the granular nature of debugging. **Code Example:** """javascript // src/components/Button.jsx import React from 'react'; function Button({ onClick, children }) { return ( <button onClick={onClick}> {children} </button> ); } export default Button; """ """javascript // cypress/component/Button.cy.js import Button from '../../src/components/Button'; describe('Button Component', () => { it('renders a button with the correct text', () => { cy.mount(<Button>Click Me</Button>); cy.get('button').should('contain', 'Click Me'); }); it('calls the onClick handler when clicked', () => { const onClickSpy = cy.spy().as('onClick'); cy.mount(<Button onClick={onClickSpy}>Click Me</Button>); cy.get('button').click(); cy.get('@onClick').should('have.been.called'); }); }); """ ## 4. Performance Considerations ### 4.1. Minimize Test Setup **Standard:** Optimize test setup to minimize execution time. * **Do This:** Use "cy.session()" to cache sessions, mock API calls to avoid unnecessary network requests, and seed the database with test data only once. * **Don't Do This:** Repeat the same setup steps for every test. This slows down your test suite unnecessarily. **Why:** Reducing test setup time dramatically improves the overall speed of the test suite. ### 4.2. Limit Network Requests **Standard:** Minimize the number of network requests made during tests. * **Do This:** Mock API calls whenever possible, especially for read-only data. Use "cy.intercept()" as previously shown. * **Don't Do This:** Rely on real APIs for every test interaction. **Why:** Network requests are a major bottleneck in E2E tests. Mocking significantly speeds up execution. ### 4.3. Test Isolation **Standard:** Ensure tests are isolated from each other. * **Do This:** Clear browser cookies and local storage before each test, and reset the database to a known state using "cy.task()". * **Don't Do This:** Allow tests to influence each other's state. **Why:** Test isolation prevents flaky tests and ensures reliable results. ## 5. Security Best Practices ### 5.1. Protect Sensitive Data **Standard:** Do not store sensitive data (e.g., passwords, API keys) directly in test files. * **Do This:** Use environment variables or Cypress configuration files to store sensitive data, and access them using "Cypress.env()" or "Cypress.config()". * **Don't Do This:** Commit sensitive data to the repository. **Why:** Protecting sensitive data prevents accidental exposure and security breaches. **Code Example:** """javascript // cypress.config.js module.exports = defineConfig({ env: { api_url: 'https://api.example.com', auth_token: process.env.AUTH_TOKEN // Set env variable on system }, e2e: { setupNodeEvents(on, config) { // implement node event listeners here }, }, }) """ """javascript // cypress/e2e/api.cy.js describe('API Tests', () => { it('fetches data from the API', () => { cy.request({ url: Cypress.env('api_url') + '/data', headers: { Authorization: "Bearer ${Cypress.env('auth_token')}" } }).then((response) => { expect(response.status).to.eq(200); }); }); }); """ ### 5.2. Validate API Responses **Standard:** Validate API responses to prevent vulnerabilities. * **Do This:** Check the status code, headers, and body of API responses to ensure they match the expected format and content. * **Don't Do This:** Assume that API calls always succeed without validation. **Why:** Validating API responses helps detect security vulnerabilities and ensures data integrity. ### 5.3. Sanitize Input **Standard:** When possible, sanitize input within your tests. * **Do This:** Before entering data into fields, particularly those which post to external APIs or databases, use Regex or similar means to sanitize inputs. * **Don't Do This:** Assume safe inputs. ## 6. Continuous Integration ### 6.1. Run Tests in CI **Standard:** Integrate Cypress tests into your CI/CD pipeline. * **Do This:** Configure your CI system (e.g., Jenkins, CircleCI, GitHub Actions) to run Cypress tests automatically on every code commit or pull request. * **Don't Do This:** Rely on manual test execution. **Why:** Automated testing in CI/CD provides early feedback on code changes and helps prevent regressions. ### 6.2. Parallelization **Standard:** Employ parallelization through Cypress Cloud or similar. * **Do This:** Use Cypress Cloud to efficiently parallelize your test execution. * **Don't Do This:** Run tests sequentially if parallelization is possible. By adhering to these coding standards, you can ensure that your Cypress tests are maintainable, performant, and secure, contributing to the overall quality and reliability of your application.
# Core Architecture Standards for Cypress This document outlines the core architectural standards for Cypress testing, focusing on project structure, organization, and design patterns to ensure maintainable, performant, and reliable tests. These standards are designed for use with the latest versions of Cypress. ## 1. Project Structure and Organization ### 1.1. Directory Structure **Standard:** Follow a modular directory structure to separate concerns and enhance readability. **Do This:** """ cypress/ ├── e2e/ # End-to-End tests │ ├── components/ # Tests for individual components │ │ ├── Button.cy.js │ │ └── Input.cy.js │ ├── integration/ # Feature/Scenario based tests │ │ ├── authentication/ │ │ │ ├── login.cy.js │ │ │ └── logout.cy.js │ │ └── products/ │ │ ├── add_to_cart.cy.js │ │ └── view_product.cy.js │ └── utils/ # Reusable test utilities │ ├── auth.js # Authentication helpers │ └── commands.js # Custom Cypress commands ├── support/ # Support files │ ├── e2e.js # Commands and configuration for E2E tests │ └── component.js # Commands and configuration for component tests ├── fixtures/ # Test data fixtures │ ├── user.json │ └── product.json cypress.config.js # Cypress configuration file """ **Don't Do This:** * Flat directory structures with all tests in a single folder. * Mixing component and E2E tests within the same directory. * Ignoring the separation of concerns (e.g., placing utilities directly in test files). **Why:** A well-structured project improves navigation, simplifies maintenance, and promotes code reuse. Separating component and integration tests allows for focused and efficient testing strategies. ### 1.2. Configuration Management **Standard:** Utilize "cypress.config.js" to manage environment-specific configurations and avoid hardcoding values. **Do This:** """javascript // cypress.config.js const { defineConfig } = require('cypress'); module.exports = defineConfig({ e2e: { baseUrl: 'https://example.com', env: { api_url: 'https://api.example.com', username: 'testuser', password: 'testpassword' }, setupNodeEvents(on, config) { // implement node event listeners here }, }, component: { devServer: { framework: 'react', bundler: 'webpack', }, }, }); """ """javascript // In a test file: describe('Login', () => { it('should login successfully', () => { cy.visit('/'); cy.get('#username').type(Cypress.env('username')); cy.get('#password').type(Cypress.env('password')); cy.get('#login-button').click(); cy.url().should('include', '/dashboard'); }); }); """ **Don't Do This:** * Hardcoding environment-specific values like URLs, usernames, or passwords directly in test files. * Failing to utilize the "env" section in "cypress.config.js" for environment variables. * Overly complex "setupNodeEvents" without proper modularization (see section 2.3.1 for enhancement through plugins) **Why:** This approach allows tests to be run in different environments without modifying the test code, promoting reusability and simplifying deployment. ### 1.3. Fixture Management **Standard:** Store test data in fixture files (".json", ".csv") and load them using "cy.fixture()". **Do This:** """json // cypress/fixtures/user.json { "username": "testuser", "password": "testpassword" } """ """javascript // In a test file: describe('Login', () => { it('should login successfully', () => { cy.fixture('user').then((user) => { cy.visit('/'); cy.get('#username').type(user.username); cy.get('#password').type(user.password); cy.get('#login-button').click(); cy.url().should('include', '/dashboard'); }); }); }); """ **Don't Do This:** * Hardcoding data directly in the test files. * Creating excessively large fixture files that are difficult to manage. * Using fixtures for settings data that should be stored in Cypress config files **Why:** Fixtures decouple test data from test logic, allowing for easy updates to data without modifying the tests themselves. ## 2. Architectural Patterns ### 2.1. Page Object Model (POM) **Standard:** Implement the Page Object Model (POM) to encapsulate UI elements and interactions on specific pages. **Do This:** """javascript // cypress/page_objects/LoginPage.js class LoginPage { visit() { cy.visit('/login'); } getUsernameField() { return cy.get('#username'); } getPasswordField() { return cy.get('#password'); } getLoginButton() { return cy.get('#login-button'); } login(username, password) { this.getUsernameField().type(username); this.getPasswordField().type(password); this.getLoginButton().click(); } } export default LoginPage; """ """javascript // In a test file: import LoginPage from '../page_objects/LoginPage'; describe('Login', () => { const loginPage = new LoginPage(); it('should login successfully', () => { loginPage.visit(); loginPage.login('testuser', 'testpassword'); cy.url().should('include', '/dashboard'); }); }); """ **Don't Do This:** * Directly referencing UI elements within test files. * Mixing UI element interactions with test assertions. * Creating 'God' Page Objects containing functionality for multiple pages **Why:** POM improves test readability, reduces code duplication, and simplifies maintenance by centralizing UI element references and interactions. Using multiple smaller page objects improve maintainability. ### 2.2. Custom Commands **Standard:** Create custom Cypress commands for reusable actions and assertions. **Do This:** """javascript // cypress/support/e2e.js Cypress.Commands.add('login', (username, password) => { cy.visit('/login'); cy.get('#username').type(username); cy.get('#password').type(password); cy.get('#login-button').click(); cy.url().should('include', '/dashboard'); }); Cypress.Commands.add('assertToastMessage', (message) => { cy.get('.toast').should('contain', message); }); """ """javascript // In a test file: describe('Login', () => { it('should login successfully', () => { cy.login('testuser', 'testpassword'); cy.url().should('include', '/dashboard'); }); it('should display a welcome message', () => { cy.login('testuser', 'testpassword'); cy.assertToastMessage('Welcome, testuser!'); }); }); """ **Don't Do This:** * Duplicating code for common actions across multiple test files. * Creating overly complex custom commands that perform multiple unrelated tasks. * Using custom commands as a substitute for proper architecture, such as page objects. **Why:** Custom commands reduce code duplication, improve test readability, and provide a consistent interface for common actions. ### 2.3. Plugins for Complex Tasks **Standard**: Use Cypress plugins to handle complex tasks that cannot be easily managed within the test code itself, or needs to interact with the OS. Typically configured inside "cypress.config.js" **Do This:** * Utilize plugins for tasks like: * Database seeding/resetting * Code coverage reporting * Mocking services * Modifying browser behavior * Interacting with file system """javascript // cypress.config.js const { defineConfig } = require("cypress"); const dbReset = require('./cypress/plugins/db-reset'); //Example of custom one module.exports = defineConfig({ e2e: { setupNodeEvents(on, config) { on('task', { dbReset: () => { return dbReset() }, }) }, }, }); """ """javascript // cypress/plugins/db-reset.js // Example of a db reset plugin using knex.js const Knex = require('knex') const db = Knex({ client: 'pg', connection: { host : '127.0.0.1', port : 5432, user : 'your_db_user', password : 'your_db_password', database : 'your_db_name' } }) module.exports = () => { return db.raw('TRUNCATE TABLE your_table CASCADE') } """ """javascript // In a test file: describe('Tests with DB', () => { it('Should start with a cleaned DB', () => { cy.task('dbReset') // ... rest of test actions }) }) """ **Don't Do This:** * Overloading plugins with unnecessary functionality. Stick to single responsibility principle. * Performing actions better handled within the test context (e.g., simple API calls). * Neglecting to keep plugins updated as Cypress versions increment. **Why:** Plugins allow for seamless integration with external tools and resources, expanding Cypress' capabilities and handling complex setups without polluting the test code. ## 3. Asynchronous Handling and Synchronization ### 3.1. Cypress Commands and Chaining **Standard:** Leverage Cypress's built-in command chaining and retry mechanisms to handle asynchronous operations. **Do This:** """javascript it('loads data and displays it', () => { cy.intercept('GET', '/api/data', { fixture: 'data.json' }).as('getData'); cy.visit('/data-page'); cy.wait('@getData'); //Explicit wait for route. cy.get('#data-list').should('be.visible').and('have.length.greaterThan', 0); }); """ **Don't Do This:** * Using "setTimeout" or other manual waiting mechanisms. * Assuming that elements are immediately available after a UI update. * Creating overly long command chains that are difficult to debug. **Why:** Cypress automatically handles asynchronous operations and retries assertions, ensuring that tests are robust and reliable. Using explicit "cy.wait()" commands on aliased routes improves reliability over "cy.wait(number)" in many situations. ### 3.2. Promises and Async/Await **Standard:** Avoid direct use of "Promise" and "async/await" within Cypress test code outside "setupNodeEvents" or plugins. Rely on Cypress' internal handling of asynchronous operations. **Do This:** """javascript //Preferred Cypress way it('should perform an action after a request completes', () => { cy.request('https://api.example.com/data').then((response) => { expect(response.status).to.eq(200); cy.get('#element').type(response.body.value); }); }); """ **Don't Do This:** * Outside "setupNodeEvents" in cypress config, using "async/await" directly within tests can lead to unexpected behavior. Use Cypress' built-in command handling or ".then()" callbacks. * Manually resolving or rejecting promises outside of plugin/task contexts. * Unnecessary nesting of "then()" calls. **Why:** Cypress manages its command queue and asynchronous operations automatically, and direct use of "Promises" can lead to race conditions. ## 4. Data Management and Handling ### 4.1. Data Factories **Standard:** Utilize a data factory pattern to generate dynamic and consistent test data. This works particularly well when setting up database states to be tested. **Do This:** """javascript // cypress/support/factories.js const faker = require('faker'); const createUser = (overrides = {}) => { return { firstName: faker.name.firstName(), lastName: faker.name.lastName(), email: faker.internet.email(), password: 'password123', ...overrides, }; }; Cypress.Commands.add('createUser', createUser); """ """javascript // In a test file: describe('User Registration', () => { it('registers a new user', () => { const user = cy.createUser({ email: 'custom@example.com' }); cy.visit('/register'); cy.get('#firstName').type(user.firstName); cy.get('#lastName').type(user.lastName); cy.get('#email').type(user.email); cy.get('#password').type(user.password); cy.get('#registerButton').click(); cy.url().should('include', '/dashboard'); }); }); """ **Don't Do This:** * Duplicating data generation logic across multiple test files. * Relying on static data that can become outdated. * Avoid using faker.js in sensitive contexts. **Why:** Data factories ensure tests use consistent and realistic data, improving test reliability and reducing the risk of false positives. ### 4.2. API Interaction for Data Setup **Standard:** Use Cypress' "cy.request()" to interact with APIs directly to set up test data or validate application state. **Do This:** """javascript // Before each test: beforeEach(() => { cy.request('POST', '/api/users', { username: 'testuser', password: 'testpassword', }).then((response) => { expect(response.status).to.eq(201); cy.wrap(response.body.id).as('userId'); // Alias for later use }); }); it('performs an action with the created user', () => { cy.get('@userId').then((userId) => { cy.visit("/user/${userId}/profile"); // ... rest of the test }); }); """ **Don't Do This:** * Setting up test data through the UI. * Ignoring API responses and assuming success. * Not cleaning up data after tests (use "afterEach" hook and "DELETE" requests if applicable). **Why:** API interaction allows for fast and reliable data setup and validation, bypassing the UI and reducing test execution time. ## 5. Error Handling and Resilience ### 5.1. Test Retries **Standard:** Configure test retries in Cypress to handle intermittent failures due to network issues or flaky UI elements. This is configured in "cypress.config.js". **Do This:** """javascript // cypress.config.js const { defineConfig } = require('cypress') module.exports = defineConfig({ retries: { runMode: 2, openMode: 0, }, e2e: { // We'll configure the baseUrl here as well. baseUrl: 'http://localhost:8080', }, }) """ **Don't Do This:** * Relying solely on retries to fix flaky tests. Address the root cause of the flakiness. * Setting excessively high retry counts, masking underlying issues. * Not logging or monitoring retried tests. **Why:** Test retries can mitigate intermittent failures, improving test stability and reducing false positives. ### 5.2. Uncaught Exception Handling **Standard:** Use "Cypress.on('uncaught:exception', ...)" to handle uncaught exceptions gracefully. This should only be used to allow tests to gracefully proceed, and not to avoid test failures. **Do This:** """javascript // cypress/support/e2e.js Cypress.on('uncaught:exception', (err, runnable) => { // returning false here prevents Cypress from // failing the test console.log('Uncaught Exception: ', err.message); return false }); """ **Don't Do This:** * Ignoring uncaught exceptions, which can lead to misleading test results. * Using uncaught exception handling to mask actual errors in the application. * Only use this approach if you fully understand why the exception happens, and are confident that it won't impact your test outcome. **Why:** Proper error handling ensures that tests fail gracefully, providing valuable information about application errors without crashing the test run. ## 6. Component Testing Architecture ### 6.1. Component Folder Structure **Standard**: Mirror your React/Vue/etc. component folder structure in your Cypress tests. This makes it easy to locate tests for specific components. **Do This:** """ src/ components/ Button/ Button.jsx Button.css Input/ Input.jsx Input.css cypress/ component/ components/ Button/ Button.cy.jsx Input/ Input.cy.jsx """ **Don't Do This:** * Putting all component tests in one flat directory * Mixing component and E2E tests in the same folder **Why**: Clear separation and mirroring of component structure allows for easy navigation. ### 6.2. Component Mounting **Standard**: Use "cy.mount()" to mount your components for testing. **Do This:** """jsx // Button.cy.jsx import Button from './Button'; import React from 'react'; it('renders', () => { cy.mount(<Button>Click me</Button>); cy.get('button').should('contain', 'Click me'); }); """ **Don't Do This:** * Mounting entire applications or large parts of the application when component testing. * Using "ReactDOM.render" directly. Use "cy.mount()". **Why**: "cy.mount()" sets up the correct environment and handles necessary cleanup. ## 7. Security Considerations ### 7.1. Sensitive Data **Standard:** Avoid storing sensitive data (API keys, passwords, etc.) directly in test code or configuration files. **Do This:** * Use environment variables to store sensitive data. * Utilize secrets management tools for secure storage and retrieval of credentials. * Make sure environment variables configured in CI/CD pipelines are properly secured. **Don't Do This:** * Committing sensitive data to version control. * Logging sensitive data in test reports. **Why:** Protecting sensitive data is crucial to prevent unauthorized access and potential security breaches. ### 7.2. Cross-Site Scripting (XSS) **Standard:** Ensure that tests do not introduce XSS vulnerabilities by sanitizing input data and validating output. Use "Cypress.Commands.addQuery('clean', ...)" to help sanitize data. **Do This:** * Escape special characters in input data. * Validate that output data does not contain malicious code. * Use code analysis tools to identify potential security risks. **Don't Do This:** * Trusting unsanitized input data. * Ignoring potential XSS vulnerabilities. **Why:** Preventing XSS vulnerabilities protects against malicious attacks and ensures the security of the application. ## 8. Code Style and Formatting ### 8.1. Consistent Formatting **Standard:** Use a code formatter (e.g., Prettier) to ensure consistent code style and formatting. """javascript // .prettierrc.js module.exports = { semi: true, trailingComma: 'all', singleQuote: true, printWidth: 120, tabWidth: 2, }; """ **Do This:** * Configure Prettier in your project and integrate it with your IDE. * Run Prettier automatically on commit or before pushing code. **Don't Do This:** * Inconsistent indentation, spacing, or line breaks. * Ignoring code formatting guidelines. **Why:** Consistent formatting improves code readability and reduces the cognitive load on developers. ### 8.2. Meaningful Names **Standard:** Use clear and descriptive names for variables, functions, and test cases. **Do This:** * Choose names that accurately reflect the purpose and functionality of the code. * Avoid abbreviations or acronyms that may be unclear to others. **Don't Do This:** * Using generic names like "data", "item", or "temp". * Omitting important details in names. **Why:** Meaningful names improve code understanding and reduce the risk of misinterpretation.
# Component Design Standards for Cypress This document outlines the coding standards for component design in Cypress, aiming to promote reusable, maintainable, and testable components. These standards should be used as a guide for developers and AI coding assistants alike to ensure consistency and quality in Cypress projects. This document targets best practices that are relevant as of the current version of Cypress. ## 1. General Principles of Component Design ### 1.1. Reusability **Standard:** Components should be designed to be reusable across multiple tests and applications. * **Do This:** Create generic components that accept props for customization. * **Don't Do This:** Bake in specific test cases or application logic directly into the component's code. **Why:** Reusable components reduce code duplication, improve maintainability, and make tests easier to understand. They provide a consistent API for interacting with UI elements. **Example:** """javascript // Good: Reusable button component // my-button.jsx import React from 'react'; function MyButton({ label, onClick, disabled, className }) { return ( <button className={"my-button ${className}"} onClick={onClick} disabled={disabled} > {label} </button> ); } export default MyButton; """ """javascript // Test usage: import MyButton from './my-button'; describe('MyButton Component', () => { it('should display the correct label', () => { cy.mount(<MyButton label="Click Me" />); cy.get('button').should('contain', 'Click Me'); }); it('should call onClick when clicked', () => { const onClickSpy = cy.spy().as('onClick'); cy.mount(<MyButton label="Click Me" onClick={onClickSpy} />); cy.get('button').click(); cy.get('@onClick').should('have.been.called'); }); }); """ """javascript // Anti-pattern: Button Component with hardcoded test logic. // DON'T DO THIS import React from 'react'; function MyButton() { const handleClick = () => { // NEVER EVER MIX APPLICATION AND TEST DOMAIN cy.log("This should never be inside an application component") } return ( <button className="my-button" onClick={handleClick} > Click Me </button> ); } export default MyButton; """ ### 1.2. Maintainability **Standard:** Components should be easy to understand, modify, and debug. * **Do This:** Use clear and descriptive names for components, props, and methods. Keep components small and focused. Write useful propTypes when using React * **Don't Do This:** Create large, complex components with intertwined logic. Avoid deeply nested structures or overly clever code. **Why:** Maintainable components reduce the cost of long-term maintenance and make it easier for new team members to contribute. **Example:** """javascript // Good: Small, focused component // my-input.jsx import React from 'react'; import PropTypes from 'prop-types'; function MyInput({ label, value, onChange, type = "text", placeholder }) { return ( <div> <label htmlFor="my-input">{label}</label> <input type={type} id="my-input" value={value} onChange={onChange} placeholder={placeholder} /> </div> ); } MyInput.propTypes = { label: PropTypes.string.isRequired, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, type: PropTypes.string, placeholder: PropTypes.string, } export default MyInput; """ """javascript // Anti-pattern: Huge, complex component. DON'T DO THIS function MegaComponent() { // ... 500 lines of spaghetti code ... return ( <div> {/* ... lots of confusing JSX ... */} </div> ); } """ ### 1.3. Testability **Standard:** Components should be easy to test in isolation with Cypress. * **Do This:** Use data attributes for targeting elements in tests. Avoid tight coupling between components. Make sure the UI elements have unique ID's where appropriate, especially in forms. * **Don't Do This:** Rely on implementation details that are likely to change. Make components depend on global state or external services without mocking. **Why:** Testable components allow for thorough testing, which reduces the risk of bugs and improves the overall quality of the application. **Example:** """javascript // Good: Data attribute for targeting function MyComponent({dataId, text}) { return ( <div data-testid={dataId}> {text} </div> ); } export default MyComponent; """ """javascript // Test usage import MyComponent from './my-component'; describe('MyComponent', () => { it('should display the text', () => { cy.mount(<MyComponent dataId="my-component" text="Hello, world!" />); cy.get('[data-testid="my-component"]').should('contain', 'Hello, world!'); }); }); """ """javascript // Anti-pattern: Relying on implementation details. DON'T DO THIS // <button onClick={() => document.getElementById("some-other-element").innerText = "Clicked!"}>Click me</button> // In the test, you are trying to find the text inside another element // IT WILL BE VERY HARD to test in the future. Instead // Use application event or something like 'onClick' to communicate with the internal button. """ ## 2. React Component Structure ### 2.1. Functional Components with Hooks **Standard:** Prefer functional components with hooks for managing state and side effects. * **Do This:** Use "useState", "useEffect", and custom hooks to manage component logic. * **Don't Do This:** Use class-based components unless there's a compelling reason. **Why:** Functional components with hooks are more concise, easier to understand, and promote code reuse through custom hooks. They align with modern React development practices. **Example:** """javascript // Good: Functional component with useState import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } export default Counter; """ """javascript // Anti-pattern: Class-based component (unnecessary) DON'T DO THIS import React, { Component } from 'react'; class Counter extends Component { constructor(props) { super(props); this.state = { count: 0 }; } increment = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <div> <p>Count: {this.state.count}</p> <button onClick={this.increment}>Increment</button> </div> ); } } export default Counter; """ ### 2.2. Prop Types **Standard:** Use PropTypes to define the expected types of component props. * **Do This:** Define PropTypes for all props that a component accepts. * **Don't Do This:** Omit PropTypes or use "any" as the prop type. **Why:** PropTypes help catch type-related errors during development, improve code readability, and provide documentation for component usage. They significantly improve the feedback loop, leading to quicker debugging. **Example:** """javascript // Good: PropTypes definition import React from 'react'; import PropTypes from 'prop-types'; function Greeting({ name, age , children }) { return ( <div> Hello, {name}! You are {age} years old. {children} </div> ); } Greeting.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number.isRequired, children: PropTypes.node }; export default Greeting; """ """javascript // Anti-pattern: Missing PropTypes. DON'T DO THIS function Greeting({ name, age }) { return ( <div> Hello, {name}! You are {age} years old. </div> ); } export default Greeting; """ ### 2.3. Component Composition **Standard:** Favor component composition over inheritance. * **Do This:** Use props.children to pass content to a component. Create higher-order components (HOCs) or render props for shared logic. * **Don't Do This:** Rely on inheritance to reuse code between components. **Why:** Component composition provides more flexibility and avoids the tight coupling associated with inheritance. It promotes modularity and reusability. **Example:** """javascript // Good: Component composition using props.children // layout.jsx import React from 'react'; function Layout({ children }) { return ( <div className="layout"> <header>Header</header> <main>{children}</main> <footer>Footer</footer> </div> ); } export default Layout; """ """javascript // Usage layout.jsx // In a page or other component: import Layout from './layout'; function HomePage() { return ( <Layout> <h1>Welcome to the Home Page!</h1> <p>Some content here.</p> </Layout> ); } export default HomePage; """ """javascript // Anti-pattern: Using inheritance. DON'T DO THIS. This pattern is more prominent in class based components. class BaseComponent extends React.Component { sharedMethod() { // ... shared logic ... } render() { return <div>Base Component</div>; } } class DerivedComponent extends BaseComponent { render() { return <div>Derived Component</div>; } } """ ## 3. Cypress-Specific Component Design Considerations ### 3.1. Data Attributes for Selectors **Standard:** Use "data-*" attributes for selecting elements in your Cypress tests. * **Do This:** Add "data-testid", "data-cy", or similar attributes to elements you want to target in tests. * **Don't Do This:** Use CSS classes or IDs unless they are specifically for styling or functional purposes, not solely for testing, because CSS class names are much more likely to change during development cycles. **Why:** "data-*" attributes provide a stable and reliable way to select elements, even if the underlying CSS or HTML structure changes. They decouple tests from styling, increasing test stability. **Example:** """javascript // Good: Using data-testid function MyComponent({ text }) { return <div data-testid="my-component">{text}</div>; } export default MyComponent; """ """javascript // Cypress Test import MyComponent from './my-component'; describe('MyComponent', () => { it('should display the text', () => { cy.mount(<MyComponent text="Hello, world!" />); cy.get('[data-testid="my-component"]').should('contain', 'Hello, world!'); }); }); """ """javascript // Anti-pattern: Relying on CSS classes for selection. DON'T DO THIS function MyComponent({ text }) { return <div className="my-component">{text}</div>; } """ ### 3.2. Component Isolation **Standard:** Ensure components can be tested in complete isolation. * **Do This:** Use Cypress component testing to mount an individual component and test its behavior. Mock any external dependencies. * **Don't Do This:** Rely on integration tests to test component behavior. **Why:** Testing components in isolation makes it easier to identify and fix bugs. It also allows you to test components more thoroughly and efficiently. **Example:** """javascript // Good: Testing a component in isolation // counter.jsx import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <p data-testid="counter-value">Count: {count}</p> <button data-testid="increment-button" onClick={() => setCount(count + 1)}>Increment</button> </div> ); } export default Counter; """ """javascript // Cypress Component Test // counter.cy.jsx import Counter from './counter'; describe('Counter Component', () => { it('should increment the count when the button is clicked', () => { cy.mount(<Counter />); cy.get('[data-testid="increment-button"]').click(); cy.get('[data-testid="counter-value"]').should('contain', 'Count: 1'); }); }); """ ### 3.3. Avoid Direct DOM Manipulation **Standard:** Avoid direct DOM manipulation within components beyond what is needed with standard React practices. * **Do This:** Rely on React's state management and rendering to update the UI. If other libraries manipulate the DOM, ensure they do not cause unexpected side effects during testing. * **Don't Do This:** Use "document.getElementById" or similar methods to directly modify the DOM within a component, except when it's crucial and well-controlled. **Why:** Direct DOM manipulation can make components harder to test and maintain, and can lead to inconsistencies between the component's state and the actual DOM. **Example:** """javascript // Anti-pattern: Direct DOM manipulation, DON'T DO THIS import React, { useRef, useEffect } from 'react'; function MyComponent() { const myRef = useRef(null); useEffect(() => { if (myRef.current) { myRef.current.textContent = 'Hello, world!'; // Direct DOM manipulation. Bad practice! } }, []); return <div ref={myRef}></div>; } export default MyComponent; """ Instead, use a "useState" hook: """javascript //Recommended Practice import React, { useState } from 'react'; function MyComponent() { const [text, setText] = useState(''); useEffect(() => { setText('Hello, world!'); }, []); return <div>{text}</div>; } export default MyComponent; """ ### 3.4 Clean Cypress Test Code **Standard:** Every test should check one, and only one, logical component pathway. Also, Cypress tests should not contain application source code. * **Do This:** Have a single "it()" block only check one specific function to limit the blast radius of any failing tests. * **Don't Do This:** Never add application source code and import components directly into Cypress files for testing. **Why:** If you add any application code into Cypress, there is now a high risk you have muddied the waters when it comes to testing. It should *only* orchestrate application code in tests. **Example:** """javascript // Anti-pattern: Application code imported into the test suite. DON'T DO THIS import { formatDate } from '../../src/utils'; // Importing application code describe('MyComponent', () => { it('should display the formatted date', () => { const date = new Date(); const formattedDate = formatDate(date); // Using application code cy.mount(<MyComponent date={date} />); cy.get('[data-testid="date"]').should('contain', formattedDate); }); }); """ In the example above, "formatDate" is part of application code and should only be called *by* the application and *invoked* by the test. """javascript //Recommended Practice - Mocking and intercepting describe('API Call Component', () => { it('should display the fetched data', () => { cy.intercept('GET', '/api/data', { fixture: 'data.json' }).as('getData'); // Mock API cy.mount(<ApiCallComponent />); cy.wait('@getData'); // Verify API call cy.get('[data-testid="data-display"]').should('contain', 'Mocked Data'); // Check against fake data }); }); """ ## 4. Optimizing Component Performance ### 4.1. Memoization **Standard:** Use "React.memo" to prevent unnecessary re-renders of components. * **Do This:** Wrap pure functional components with "React.memo". Provide a custom comparison function if the default shallow comparison is not sufficient. * **Don't Do This:** Memoize components indiscriminately. Only memoize components that are likely to re-render with the same props. **Why:** Memoization can significantly improve performance by preventing unnecessary re-renders. This is particularly important for complex components or components that are frequently re-rendered. **Example:** """javascript // Good: Using React.memo import React from 'react'; const MyComponent = React.memo(function MyComponent({ value }) { console.log('Rendering MyComponent with value:', value); return <div>Value: {value}</div>; }); export default MyComponent; """ """javascript // Example with a custom comparison function: import React from 'react'; const MyComponent = React.memo( function MyComponent({ data }) { console.log('Rendering MyComponent with data:', data); return <div>Data: {data.name}</div>; }, (prevProps, nextProps) => { // Custom comparison function return prevProps.data.id === nextProps.data.id; } ); export default MyComponent; """ ### 4.2. Code Splitting **Standard:** Use code splitting to reduce the initial load time of the application. * **Do This:** Use "React.lazy" and "Suspense" to load components on demand. Split large components into smaller chunks. * **Don't Do This:** Load all components upfront. **Why:** Code splitting reduces the amount of JavaScript that needs to be downloaded and parsed initially, which improves the initial load time and responsiveness of the application. **Example:** """javascript // Good: Using React.lazy and Suspense import React, { lazy, Suspense } from 'react'; const MyLazyComponent = lazy(() => import('./my-lazy-component')); function MyPage() { return ( <div> <h1>My Page</h1> <Suspense fallback={<div>Loading...</div>}> <MyLazyComponent /> </Suspense> </div> ); } export default MyPage; """ ### 4.3. Virtualization **Standard:** Use virtualization techniques for rendering large lists or tables. * **Do This:** Use libraries like "react-window" or "react-virtualized" to render only the visible items in a large list or table. * **Don't Do This:** Render all items in a large list or table at once. **Why:** Virtualization significantly improves performance when rendering large lists or tables by reducing the number of DOM elements that need to be created and updated. **Example:** """javascript // Good: Using react-window import React from 'react'; import { FixedSizeList } from 'react-window'; const Row = ({ index, style }) => ( <div style={style}>Row {index}</div> ); function MyList() { return ( <FixedSizeList height={400} width={300} itemSize={30} itemCount={1000}> {Row} </FixedSizeList> ); } export default MyList; """ ## 5. Security Considerations ### 5.1. Preventing Cross-Site Scripting (XSS) **Standard:** Sanitize user-provided input to prevent XSS attacks. * **Do This:** Use a library like "DOMPurify" to sanitize any user-provided input that is rendered in the component. * **Don't Do This:** Render unsanitized user input directly into the DOM. **Why:** XSS attacks can allow malicious users to inject arbitrary JavaScript code into your application, which can compromise user data and security. **Example:** """javascript // Good: Sanitizing user input with DOMPurify import React from 'react'; import DOMPurify from 'dompurify'; function MyComponent({ userInput }) { const sanitizedInput = DOMPurify.sanitize(userInput); return <div dangerouslySetInnerHTML={{ __html: sanitizedInput }}></div>; } export default MyComponent; """ ### 5.2. Avoiding Insecure Dependencies **Standard:** Keep dependencies up-to-date and scan for vulnerabilities. * **Do This:** Regularly update dependencies to the latest versions. Use tools like "npm audit" or "yarn audit" to scan for vulnerabilities. * **Don't Do This:** Use outdated dependencies with known vulnerabilities. **Why:** Insecure dependencies can introduce security vulnerabilities into your application. ## 6. Accessibility Considerations ### 6.1. Semantic HTML **Standard:** Use semantic HTML elements to improve accessibility. * **Do This:** Use elements like "<article>", "<nav>", "<aside>", and "<footer>" to structure the content of your components. Use labels in forms. * **Don't Do This:** Use "<div>" elements for everything. **Why:** Semantic HTML elements provide structure and meaning to the content of your components, which improves accessibility for users with disabilities. **Example:** """javascript // Good: Using semantic HTML function MyArticle({ title, content }) { return ( <article> <h1>{title}</h1> <p>{content}</p> </article> ); } export default MyArticle; """ """javascript // Anti-pattern: Using divs. DON'T DO THIS function MyArticle({ title, content }) { return ( <div> <div>{title}</div> <div>{content}</div> </div> ); } export default MyArticle; """ ### 6.2. ARIA Attributes **Standard:** Use ARIA attributes to provide additional information to assistive technologies. * **Do This:** Use attributes like "aria-label", "aria-describedby", and "aria-hidden" to provide additional context for users with disabilities. * **Don't Do This:** Overuse ARIA attributes or use them incorrectly. Rely on semantic HTML elements whenever possible. **Why:** ARIA attributes allow you to provide additional information to assistive technologies, which can improve the accessibility of your components. **Example:** """javascript // Good: Using aria label import React from 'react'; function MyComponent() { return ( <button aria-label="Close dialog"> X </button> ); } export default MyComponent; """ Adhering to these guidelines will help to create robust, maintainable, and testable Cypress components. The examples illustrate the practical application of each standard, enabling developers and AI coding assistants to produce high-quality code.
# State Management Standards for Cypress This document outlines the coding standards for state management in Cypress tests. Effective state management is crucial for writing robust, maintainable, and performant end-to-end tests. These standards promote clear data flow, minimize flaky tests, and improve overall test suite reliability. ## 1. Principles of State Management in Cypress State management in Cypress refers to how test data, application state, and context are handled across different test cases and within a single test. Due to the nature of end-to-end testing, direct manipulation of application state is often necessary. However, indiscriminate manipulation can lead to unpredictable and unreliable results. * **Explicit State Setup:** Tests should explicitly set up the application state needed for their execution. Implicit dependencies on previous tests or a specific initial state should be avoided. * **Isolated Test Cases:** Each test case should operate independently. It should clean up or reset any state it modifies to avoid interfering with subsequent tests. * **Data Fixtures for Predictability:** Use data fixtures to provide consistent and well-defined data for tests. This ensures tests are not reliant on dynamically generated or changing data, making them more deterministic. * **Minimize Global State:** Keep the use of global variables or shared state to a minimum. When shared state is necessary, manage it carefully using Cypress's built-in features or custom commands. * **Controlled Side Effects:** Be aware of the side effects that test actions may have on the application state. Ensure these side effects are predictable and do not inadvertently affect other tests. ## 2. Setting Up Initial Application State ### 2.1. Using "cy.request" for Database Seeding or API Calls For complex applications, seeding the database or making API calls before running a test or suite of tests is often the most efficient way to set up the required state. **Do This:** Use "cy.request" to seed the database or make API calls that create, modify, or delete data. **Why:** Provides a clean and predictable application state for each test, reducing the risk of flaky tests due to inconsistent data. """javascript // Seed the database before running a suite of tests before(() => { cy.request('POST', '/api/seed', { users: 10, products: 50 }); }); // Create a specific user before each test beforeEach(() => { cy.request('POST', '/api/users', { name: 'John Doe', email: 'john.doe@example.com' }); }); it('should display the user profile', () => { cy.visit('/profile/john.doe@example.com'); cy.contains('John Doe').should('be.visible'); }); """ **Don't Do This:** Rely on the UI to set up complex application states. It is slow and can be unpredictable. **Anti-pattern:** """javascript // Avoid this approach it('should create a new user through the UI', () => { cy.visit('/register'); cy.get('#name').type('John Doe'); cy.get('#email').type('john.doe@example.com'); cy.get('#password').type('password'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/profile'); }); """ ### 2.2. Using Fixtures for Static Data When you need to use static data for testing, such as user credentials or product information, use fixtures. **Do This:** Create a fixture file (".json") and load it using "cy.fixture()". **Why:** Keeps test data separate from test logic, making tests easier to read and maintain. Fixtures enable you to easily change test data without modifying the test code directly. """json // fixtures/user.json { "name": "Jane Doe", "email": "jane.doe@example.com", "password": "password123" } """ """javascript // Load the fixture in your test beforeEach(() => { cy.fixture('user').as('user'); }); it('should log in with fixture data', () => { cy.visit('/login'); cy.get('@user').then((user) => { cy.get('#email').type(user.email); cy.get('#password').type(user.password); cy.get('button[type="submit"]').click(); cy.url().should('include', '/profile'); }); }); """ **Don't Do This:** Hardcode data directly in the tests or generate it dynamically without a clear strategy. **Anti-pattern:** """javascript // Avoid this approach it('should log in with hardcoded data', () => { cy.visit('/login'); cy.get('#email').type('jane.doe@example.com'); cy.get('#password').type('password123'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/profile'); }); """ ### 2.3. Using "cy.session()" to Persist Authentication State If your application requires authentication, use "cy.session()" to cache and reuse sessions between tests, which significantly speeds up test execution. **Do This:** Define a session with the necessary steps to log in and then reuse it across multiple tests. **Why:** Reduces the overhead of logging in repeatedly in each test, speeding up test execution and making tests more efficient. """javascript // Define a session for logging in beforeEach(() => { cy.session('login', () => { cy.visit('/login'); cy.get('#email').type('jane.doe@example.com'); cy.get('#password').type('password123'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/profile'); cy.contains('Jane Doe').should('be.visible'); // Add assertion to confirm login }); }); it('should display the user profile', () => { cy.visit('/profile'); // No login steps needed cy.contains('Jane Doe').should('be.visible'); }); it('should navigate to settings', () => { cy.visit('/settings'); // No login steps needed cy.contains('Settings').should('be.visible'); }); """ **Don't Do This:** Log in repeatedly in each test using UI interactions. **Anti-pattern:** """javascript // Avoid this approach it('should display the user profile (repeated login)', () => { cy.visit('/login'); cy.get('#email').type('jane.doe@example.com'); cy.get('#password').type('password123'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/profile'); cy.contains('Jane Doe').should('be.visible'); }); """ ## 3. Managing Application State During Tests ### 3.1. Using "cy.intercept" to Mock API Responses Mocking API responses using "cy.intercept" allows you to control the data your application receives, making tests more predictable and faster. **Do This:** Intercept API requests and provide predefined responses. **Why:** Decouples tests from the backend, allowing you to test UI behavior under various scenarios without relying on a live API. Speeds up test execution by avoiding network latency. Allows for testing edge cases or error conditions that might be difficult to reproduce with a live API. """javascript it('should display a list of products', () => { cy.intercept('GET', '/api/products', { fixture: 'products.json' }).as('getProducts'); cy.visit('/products'); cy.wait('@getProducts'); // Optionally wait for the route to complete cy.get('.product-item').should('have.length', 3); // Assuming products.json has 3 items }); """ **Don't Do This:** Depend on the live API for critical data during tests. **Anti-pattern:** """javascript // Avoid this approach if the API is unreliable it('should display a list of products (relying on live API)', () => { cy.visit('/products'); cy.get('.product-item').should('have.length.greaterThan', 0); }); """ ### 3.2. Clearing Application Data with "cy.clearCookies", "cy.clearLocalStorage", and "cy.clearAllSessionStorage" To ensure test isolation, it is crucial to clear cookies and local/session storage between tests. **Do This:** Use "cy.clearCookies()", "cy.clearLocalStorage()", and "cy.clearAllSessionStorage()" in "beforeEach" or "afterEach" hooks. **Why:** Prevents state from leaking between tests, ensuring each test starts from a clean slate. """javascript beforeEach(() => { cy.clearCookies(); cy.clearLocalStorage(); cy.clearAllSessionStorage(); }); it('should perform an action in an isolated session', () => { cy.visit('/'); // ... }); """ **Don't Do This:** Forget to clear cookies or storage, leading to unpredictable test behavior. ### 3.3. Using "cy.reload" for Specific Scenarios In certain cases, you might need to reload the page to reflect changes in the application state. **Do This:** Use "cy.reload()" to refresh the page and update the UI. **Why:** Allows the test to reflect changes made to the application state programmatically or through external actions. Note: Extensive use often indicates potential state mismanagement issues. """javascript it('should update the UI after reloading the page', () => { cy.visit('/settings'); cy.get('#theme-toggle').click(); cy.reload(); cy.get('body').should('have.class', 'dark-theme'); }); """ **Don't Do This:** Use "cy.reload()" indiscriminately, as it can slow down tests and indicate a problem in how the application state is being managed or tested. ### 3.4. Using "cy.task" for Backend Interactions Interact with the backend (e.g., database, file system) using "cy.task". **Do This:** Define tasks in "cypress.config.js" and call them from your tests. **Why:** Provides a secure and controlled way to interact with the backend, separating test logic from backend interactions. """javascript // cypress.config.js module.exports = defineConfig({ e2e: { setupNodeEvents(on, config) { on('task', { resetDatabase: () => { // Code to reset the database return null; }, createUser: (user) => { // Code to create a user in the database return null; }, }); }, }, }); """ """javascript // In your test beforeEach(() => { cy.task('resetDatabase'); cy.task('createUser', { name: 'John Doe', email: 'john.doe@example.com' }); }); it('should display the user profile', () => { cy.visit('/profile/john.doe@example.com'); cy.contains('John Doe').should('be.visible'); }); """ **Don't Do This:** Directly interact with the backend from your tests, which can expose sensitive information and make tests harder to maintain. ### 3.5. Avoid DOM-based State Management Cypress automatically retries commands until assertions pass, which is incredibly powerful but can be problematic if you're relying on the DOM *itself* as the source of truth for state. **Example Anti-Pattern (Avoid this):** """javascript // ANTI-PATTERN: Relying on the DOM to determine state. Fragile & slow. it('should disable the button after two clicks', () => { cy.get('#myButton').should('not.be.disabled'); cy.get('#myButton').click(); // First click (perhaps increments a counter) cy.get('#myButton').click(); // Second click (disables the button) cy.get('#myButton').should('be.disabled'); // Hope the logic is fast enough! }); """ **Why it's bad:** * **Race conditions:** The DOM might not update *immediately* after the clicks, leading to intermittent failures. Cypress will keep retrying the ".should('be.disabled')" assertion, but if the button gets disabled *just* after Cypress checks, you'll get a false negative. * **Test brittleness:** Changes to the UI rendering (e.g., CSS transitions, animations) can affect the timing and break the test. * **Slow tests:** Cypress spends extra time retrying because it doesn't have a reliable understanding of when the underlying state has converged. **Do This: Query the Application State Directly** Ideally, the application should expose endpoints or mechanisms to retrieve the underlying state directly, rather than relying on the DOM. "cy.request" or "cy.intercept" provide far more robust means of verifying application behavior. **Better Example:** """javascript it('should disable the button after two clicks', () => { // Stub the API call that the 'button' makes to track # of clicks cy.intercept('POST', '/api/buttonClick', (req) => { req.reply({statusCode: 200, body: { clickCount: req.body.clickCount + 1} }); }).as('buttonClick'); // Giving stub an Alias // Stub a GET that returns the status of the button cy.intercept('GET', '/api/buttonStatus', {disabled: false}).as('getButtonState'); cy.get('#myButton').should('not.be.disabled'); cy.get('#myButton').click(); cy.wait('@buttonClick'); // Wait for the stubbed POST cy.intercept('GET', '/api/buttonStatus', {disabled: false}).as('getButtonState'); // Restub so that next 'wait retrieves correct value cy.get('#myButton').click(); cy.wait('@buttonClick'); // Wait for this call to complete // Now, assert button is disabled - by verifying what state the app returns cy.request('GET', '/api/buttonStatus').then((response) => { expect(response.body.disabled).to.eq(true); }); }); """ **Explanation:** 1. **Stub Endpoints:** The example uses "cy.intercept" to stub out the API call for the button to report how many clicks have occurred. 2. **Direct App State Check:** Instead of waiting for the DOM to update, "cy.request" is used to make a secure call to find out exactly how many clicks have occurred. The test then compares the stubbed return to a known value. **Benefits:** * **Reliability:** No more race conditions or timing issues. The test directly verifies the underlying application state. * **Speed:** Cypress doesn't waste time retrying DOM-based assertions. * **Maintainability:** The test is robust against UI changes. It focuses on the core application logic. ## 4. Custom Commands for State Management ### 4.1. Creating Custom Commands You can encapsulate common state management operations into custom commands for reusability. **Do This:** Define custom commands in "cypress/support/commands.js". **Why:** Reduces code duplication, making tests easier to read and maintain. Provides a consistent interface for interacting with the application state. """javascript // cypress/support/commands.js Cypress.Commands.add('seedDatabase', (data) => { cy.request('POST', '/api/seed', data); }); Cypress.Commands.add('createUser', (user) => { cy.request('POST', '/api/users', user); }); // In your test beforeEach(() => { cy.seedDatabase({ users: 5, products: 20 }); cy.createUser({ name: 'John Doe', email: 'john.doe@example.com' }); }); it('should display the user profile', () => { cy.visit('/profile/john.doe@example.com'); cy.contains('John Doe').should('be.visible'); }); """ ### 4.2. Using Custom Commands to Wrap Complex Logic Wrap complex state management tasks, such as authentication flows or data setup, into custom commands. **Why:** Simplifies tests and abstracts away implementation details, improving readability and maintainability. """javascript Cypress.Commands.add('login', (email, password) => { cy.visit('/login'); cy.get('#email').type(email); cy.get('#password').type(password); cy.get('button[type="submit"]').click(); cy.url().should('include', '/profile'); cy.contains('Welcome').should('be.visible'); }); it('should log in with valid credentials', () => { cy.login('jane.doe@example.com', 'password123'); cy.contains('Jane Doe').should('be.visible'); }); """ ## 5. Advanced State Management Techniques ### 5.1. Using "Cypress.env" for Configuration Store configuration settings, such as API endpoints or authentication details, in "cypress.config.js" and access them using "Cypress.env". **Do This:** Define environment variables in "cypress.config.js" and access them in your tests. **Why:** Makes tests more portable and allows you to easily switch between different environments (e.g., development, staging, production). """javascript // cypress.config.js module.exports = defineConfig({ env: { apiUrl: 'http://localhost:3000/api', adminEmail: 'admin@example.com', }, e2e: { //... }, }); """ """javascript // In your test it('should fetch data from the API', () => { cy.request("${Cypress.env('apiUrl')}/products").then((response) => { expect(response.status).to.eq(200); }); }); """ **Don't Do This:** Hardcode configuration settings directly in the tests. ### 5.2. Using State Management Libraries (with Caution) While Cypress is primarily an end-to-end testing tool, you can integrate state management libraries like Redux or Vuex if needed. However, this should be done with caution and only when necessary. **Do This:** If your application heavily relies on a state management library, consider using it in your tests as well, but be mindful of the added complexity and potential for slower test execution. **Why:** Allows you to test the interactions between your UI components and the application state more comprehensively. """javascript // Example: Accessing Redux store from Cypress it('should update the Redux state', () => { cy.window().its('store').invoke('dispatch', { type: 'INCREMENT' }); cy.window().its('store').invoke('getState').its('count').should('eq', 1); }); """ **Don't Do This:** Overcomplicate your tests by unnecessarily introducing state management libraries. Aim to keep your tests as simple and focused as possible. End-to-end tests are better used for testing *outcomes* rather than the internal implementation of the state management system. ## 6. Conclusion Adhering to these state management standards will lead to more robust, maintainable, and performant Cypress tests. By explicitly managing application state, isolating test cases, and using fixtures and mocking techniques, you can ensure that your tests are reliable and provide valuable feedback on your application's behavior. Remember that the goal is to create tests that accurately reflect real-world user interactions and provide confidence in your application's functionality.