Skip to content

Testing

George Dawoud edited this page Nov 18, 2025 · 2 revisions

Cypress Testing Guide

ChurchCRM uses Cypress.io for comprehensive end-to-end testing of both UI components and APIs.

🚨 Important: All new code and pull requests must include appropriate test cases to ensure code quality and prevent regressions.

Related: See Automated Testing Overview for testing philosophy and strategy.

Why Cypress?

  • Fast & Reliable: Real browser testing with excellent debugging
  • Easy to Write: Clear, JavaScript-based test syntax
  • Great Developer Experience: Interactive mode for test development
  • Complete Coverage: UI, API, authentication, and integration tests
  • CI/CD Ready: Automatic test runs on PRs and commits

Overview

The Cypress testing framework covers:

  • UI Testing: End-to-end user interface tests
  • API Testing: RESTful API endpoint validation
  • Authentication Testing: Login flows and permission validation
  • Integration Testing: Cross-component functionality
  • Data Validation: CRUD operations and data integrity

Quick Start

Prerequisites

Running Tests

# Start testing environment
npm run docker:test:start

# Run all Cypress tests (headless)
npm run test

# Run tests interactively (for development & debugging)
npx cypress open --config-file docker/cypress.config.ts

# Stop testing environment when done
npm run docker:test:stop

Interactive Mode (Recommended for Development)

# Opens Cypress GUI for interactive test development
npx cypress open --config-file docker/cypress.config.ts

# In the GUI:
# 1. Select E2E Testing
# 2. Choose your browser (Chrome recommended)
# 3. Click a test file to run it interactively
# 4. See real-time test execution with time-travel debugging

Debugging Tests

When a test fails, use these tools:

// Pause execution to inspect state
cy.pause();

// Take a screenshot for visual debugging
cy.screenshot('my-test-screenshot');

// Log values to browser console
cy.log('User ID: ' + userId);

// Inspect element in the DOM
cy.get('#myElement').debug();

// Get the current value of an element
cy.get('input[name="email"]').invoke('val').then(val => {
    cy.log('Email value: ' + val);
});

Troubleshooting common test failures:

Issue Solution
Test times out Use cy.contains() for waits instead of cy.wait()
Element not found Use cy.debug() to inspect DOM; check element selectors
401 Unauthorized Verify API keys in cypress.config.ts; check auth setup
Flaky tests Add explicit waits; avoid hardcoded delays; use unique test data
Database errors Run npm run docker:test:start to reset DB; check test data cleanup

Test Structure

Tests are organized in the cypress/e2e/ directory:

cypress/e2e/
├── api/                    # API endpoint tests
│   ├── public/            # Public API tests
│   └── private/           # Authenticated API tests
├── ui/                    # User interface tests
│   ├── admin/             # Admin-specific UI tests
│   ├── people/            # People/Family management
│   ├── finance/           # Financial features
│   ├── events/            # Calendar and events
│   └── user/              # Standard user features
├── guest/                 # Unauthenticated user tests  
└── xReset/                # System reset tests

Writing UI Tests

Basic UI Test Structure

/// <reference types="cypress" />

describe("Feature Name", () => {
    it("should perform specific action", () => {
        // Login with appropriate user
        cy.loginStandard("path/to/page");
        
        // Test page content
        cy.contains("Expected Text");
        
        // Interact with elements
        cy.get("#elementId").click();
        cy.get("input[name='field']").type("test value");
        
        // Assert results
        cy.url().should("contain", "expected-path");
        cy.contains("Success Message");
    });
});

Authentication Commands

ChurchCRM provides custom authentication commands:

// Admin user (full permissions)
cy.loginAdmin("page-path");

// Standard user (limited permissions)  
cy.loginStandard("page-path");

// Custom user login
cy.login("username", "password", "page-path");

Common UI Testing Patterns

Form Testing

it("should create new person", () => {
    cy.loginStandard("PersonEditor.php");
    
    // Fill form fields
    cy.get("#FirstName").type("John");
    cy.get("#LastName").type("Doe");
    cy.get("#Gender").select("1");
    cy.get("#Email").type("[email protected]");
    
    // Submit form
    cy.get("#PersonSaveButton").click();
    
    // Verify redirect and content
    cy.url().should("contain", "PersonView.php");
    cy.contains("John Doe");
});

Navigation Testing

it("should navigate between pages", () => {
    cy.loginStandard("v2/dashboard");
    cy.contains("Welcome to");
    
    // Click navigation elements
    cy.get(".nav-link").contains("People").click();
    cy.url().should("contain", "v2/people");
    
    // Verify page content
    cy.contains("Active People List");
});

Data Interaction Testing

it("should filter and search data", () => {
    cy.loginStandard("v2/people");
    
    // Use search functionality
    cy.get("#members_filter input").type("[email protected]");
    cy.contains("John Doe");
    
    // Clear search
    cy.get("#members_filter input").clear();
});

Element Selection Best Practices

  1. Prefer data attributes: Use data-cy or data-testid attributes

    cy.get("[data-cy=submit-button]").click();
  2. Use semantic selectors: Target elements by their semantic meaning

    cy.contains("Save Changes").click();
    cy.get("button[type=submit]").click();
  3. Avoid brittle selectors: Don't use CSS classes or complex DOM paths

    // ❌ Brittle
    cy.get(".btn.btn-primary.btn-lg").click();
    
    // ✅ Better
    cy.get("#saveButton").click();

Writing API Tests

Basic API Test Structure

/// <reference types="cypress" />

describe("API Feature", () => {
    it("should handle API endpoint", () => {
        cy.makePrivateUserAPICall(
            "GET",
            "/api/endpoint",
            null,
            200
        ).then((response) => {
            expect(response).to.have.property('data');
            expect(response.data).to.be.an('array');
        });
    });
});

API Authentication

ChurchCRM provides API testing commands with built-in authentication:

// Admin API calls (full permissions)
cy.makePrivateAdminAPICall("POST", "/api/admin/endpoint", data, 201);

// User API calls (standard permissions)
cy.makePrivateUserAPICall("GET", "/api/user/profile", null, 200);

// Public API calls (no authentication)
cy.apiRequest({
    method: "GET",
    url: "/api/public/data/countries"
}).then((response) => {
    expect(response.status).to.eq(200);
});

API Testing Patterns

CRUD Operations

describe("User Management API", () => {
    it("should create, read, update, delete user", () => {
        const userData = {
            firstName: "Test",
            lastName: "User",
            email: "[email protected]"
        };
        
        // Create
        cy.makePrivateAdminAPICall("POST", "/api/users", userData, 201)
            .then((response) => {
                const userId = response.id;
                
                // Read
                cy.makePrivateAdminAPICall("GET", `/api/users/${userId}`, null, 200)
                    .then((user) => {
                        expect(user.email).to.eq(userData.email);
                    });
                
                // Update
                const updateData = { firstName: "Updated" };
                cy.makePrivateAdminAPICall("PUT", `/api/users/${userId}`, updateData, 200);
                
                // Delete
                cy.makePrivateAdminAPICall("DELETE", `/api/users/${userId}`, null, 204);
            });
    });
});

Error Handling

it("should handle validation errors", () => {
    const invalidData = { email: "invalid-email" };
    
    cy.makePrivateAdminAPICall("POST", "/api/users", invalidData, 400)
        .then((response) => {
            expect(response.errors).to.exist;
            expect(response.errors.email).to.contain("Invalid email format");
        });
});

it("should handle unauthorized access", () => {
    cy.apiRequest({
        method: "GET",
        url: "/api/admin/sensitive-endpoint"
    }).then((response) => {
        expect(response.status).to.eq(401);
    });
});

Data Validation

it("should validate API response structure", () => {
    cy.makePrivateUserAPICall("GET", "/api/people", null, 200)
        .then((response) => {
            expect(response).to.have.property('data');
            expect(response).to.have.property('pagination');
            expect(response.data).to.be.an('array');
            
            if (response.data.length > 0) {
                const person = response.data[0];
                expect(person).to.have.property('id');
                expect(person).to.have.property('firstName');
                expect(person).to.have.property('lastName');
                expect(person).to.have.property('email');
            }
        });
});

Test Data Management

Using Dynamic Data

describe("Dynamic Test Data", () => {
    it("should use unique identifiers", () => {
        const uniqueSeed = Date.now().toString();
        const testData = {
            name: `Test Family ${uniqueSeed}`,
            email: `test.${uniqueSeed}@example.com`
        };
        
        cy.loginStandard("FamilyEditor.php");
        cy.get("#FamilyName").type(testData.name);
        cy.get('input[name="Email"]').type(testData.email);
        cy.get("input[id='FamilySubmitBottom']").click();
        
        cy.contains(testData.name);
    });
});

Test Data Cleanup

describe("Data Cleanup", () => {
    let createdResourceId;
    
    it("should create test resource", () => {
        cy.makePrivateAdminAPICall("POST", "/api/resources", testData, 201)
            .then((response) => {
                createdResourceId = response.id;
            });
    });
    
    after(() => {
        // Cleanup after test completion
        if (createdResourceId) {
            cy.makePrivateAdminAPICall("DELETE", `/api/resources/${createdResourceId}`, null, 204);
        }
    });
});

Test Requirements for Pull Requests

✅ PR Testing Checklist

Your PR will NOT be merged unless:

  • All tests pass - Run npm run test before submitting
  • New tests included - For any new features or bug fixes
  • Existing tests updated - If you changed existing functionality
  • Tests are descriptive - Clear names explaining what is being tested
  • Test data is cleaned up - No leftover test data in database
  • No hardcoded waits - Use cy.contains() or explicit waits instead
  • Tests are independent - Can run in any order

Why Tests Matter

Tests provide:

  • 🛡️ Protection against regressions - Prevents old bugs from reappearing
  • 📝 Documentation - Tests show how features are supposed to work
  • 🚀 Confidence - Team can deploy with confidence
  • Faster reviews - Clear tests make code review easier
  • 💰 Saves time - Catches bugs before production

New Feature Requirements

Every new feature must include tests for:

  1. UI Tests: Cover all user interactions and edge cases
  2. API Tests: Validate all endpoints and data operations
  3. Permission Tests: Verify access controls work correctly
  4. Error Handling: Test validation and error scenarios
  5. Integration Tests: Ensure feature works with existing functionality

Example: Complete Feature Test Suite

// ui/admin/new-feature.spec.js
describe("New Feature UI", () => {
    it("should display feature page", () => {
        cy.loginAdmin("NewFeature.php");
        cy.contains("New Feature Dashboard");
    });
    
    it("should create new item", () => {
        cy.loginAdmin("NewFeature.php");
        cy.get("#createNew").click();
        // ... form interactions
        cy.contains("Item created successfully");
    });
    
    it("should handle validation errors", () => {
        cy.loginAdmin("NewFeature.php");
        cy.get("#createNew").click();
        cy.get("#submit").click(); // Submit empty form
        cy.contains("This field is required");
    });
});

// api/private/new-feature.spec.js  
describe("New Feature API", () => {
    it("should create item via API", () => {
        cy.makePrivateAdminAPICall("POST", "/api/new-feature", validData, 201);
    });
    
    it("should reject invalid data", () => {
        cy.makePrivateAdminAPICall("POST", "/api/new-feature", invalidData, 400);
    });
    
    it("should require authentication", () => {
        cy.apiRequest({
            method: "POST", 
            url: "/api/new-feature",
            body: validData
        }).then((resp) => {
            expect(resp.status).to.eq(401);
        });
    });
});

Code Coverage Guidelines

  • UI Tests: Cover all user paths and error scenarios
  • API Tests: Test all HTTP methods and response codes
  • Edge Cases: Include boundary conditions and error states
  • Permissions: Verify admin vs. user access restrictions
  • Data Validation: Test field validation and constraints

Advanced Testing Patterns

Network Interception

it("should handle network requests", () => {
    cy.intercept("POST", "/api/save-data", { statusCode: 200 }).as("saveData");
    
    cy.loginStandard("DataEntry.php");
    cy.get("#saveButton").click();
    
    cy.wait("@saveData");
    cy.contains("Data saved successfully");
});

File Upload Testing

it("should upload files", () => {
    cy.loginAdmin("FileUpload.php");
    
    const fileName = "test-file.pdf";
    cy.get('input[type="file"]').selectFile(`cypress/fixtures/${fileName}`);
    cy.get("#uploadButton").click();
    
    cy.contains("File uploaded successfully");
});

Download Verification

it("should download reports", () => {
    cy.loginAdmin("Reports.php");
    cy.get("#generateReport").click();
    
    cy.verifyDownload(".pdf", { contains: true });
});

Best Practices

Test Organization

  • Use descriptive test names that explain the expected behavior
  • Group related tests in logical describe blocks
  • Use consistent naming conventions across test files
  • Keep tests focused on single functionality

Test Reliability

  • Avoid hardcoded waits; use cy.contains() for dynamic content
  • Make tests independent and able to run in any order
  • Use unique test data to prevent conflicts
  • Clean up test data after test completion

Performance

  • Use cy.intercept() to mock slow external services
  • Minimize test data setup and teardown
  • Run tests in parallel when possible
  • Focus on critical user paths for smoke tests

Debugging

  • Use cy.pause() for interactive debugging
  • Add cy.screenshot() for visual debugging
  • Use descriptive assertions with clear error messages
  • Log important values with cy.log()

Configuration

Cypress Configuration

The main configuration is in docker/cypress.config.ts:

export default defineConfig({
  chromeWebSecurity: false,
  video: false,
  pageLoadTimeout: 120000,
  defaultCommandTimeout: 60000,
  viewportHeight: 1080,
  viewportWidth: 1920,
  retries: 4,
  env: {
    'admin.api.key': 'test-admin-key',
    'user.api.key': 'test-user-key',
  },
  e2e: {
    baseUrl: 'http://localhost/',
    specPattern: [
      'cypress/e2e/api/**/*.spec.js',
      'cypress/e2e/ui/**/*.spec.js'
    ]
  }
});

Custom Commands

Located in cypress/support/ directory:

  • ui-commands.js: Login and UI interaction commands
  • api-commands.js: API testing utilities
  • commands.d.ts: TypeScript type definitions

Continuous Integration

Tests run automatically on:

  • Every pull request
  • Commits to main branch
  • Scheduled nightly builds

CI Requirements

  • All tests must pass before merge
  • New features require corresponding tests
  • Test coverage should not decrease
  • Performance tests should not regress

Troubleshooting

Common Issues

Tests failing locally but passing in CI:

  • Ensure Docker environment is properly set up: npm run docker:test:start
  • Check that local database has correct test data
  • Verify environment variables are configured correctly
  • Try running: npm run docker:test:stop then npm run docker:test:start to reset

Flaky tests (intermittent failures):

  • Add explicit waits for dynamic content instead of hardcoded cy.wait()
  • Use cy.contains() or cy.get().should() for element waits
  • Ensure test data is properly isolated between tests
  • Avoid timing-dependent assertions

API tests returning 401 Unauthorized:

  • Verify API keys are configured in cypress.config.ts
  • Check that authentication endpoints are accessible
  • Ensure test user accounts exist in test database
  • Try resetting Docker: npm run docker:test:stop && npm run docker:test:start

"Element not found" or "Timed out" errors:

  • Use cy.debug() to inspect the DOM
  • Verify element selectors are correct
  • Check for dynamic content that needs explicit waits
  • Use cy.contains('text') instead of complex selectors

File upload tests failing:

  • Verify test fixtures exist in cypress/fixtures/
  • Check file permissions on fixture files
  • Use cy.selectFile() instead of deprecated attachFile()

Debugging strategy:

  1. Run test in interactive mode: npx cypress open --config-file docker/cypress.config.ts
  2. Pause at failure point: cy.pause()
  3. Check browser console and command log
  4. Take screenshot: cy.screenshot()
  5. Verify element is in DOM using Chrome DevTools

Debug Commands

# Run specific test file
npx cypress run --spec "cypress/e2e/ui/people/*.spec.js"

# Run tests with video recording
npx cypress run --record --video

# Open Cypress GUI for debugging
npx cypress open --config-file docker/cypress.config.ts

Examples and Templates

Feature Test Template

Use this template for new feature tests:

/// <reference types="cypress" />

describe("Feature Name", () => {
    beforeEach(() => {
        // Setup common to all tests
    });

    describe("UI Tests", () => {
        it("should display feature correctly", () => {
            // Test UI rendering
        });
        
        it("should handle user interactions", () => {
            // Test form submissions, clicks, etc.
        });
        
        it("should validate user input", () => {
            // Test form validation
        });
    });

    describe("API Tests", () => {
        it("should handle GET requests", () => {
            // Test data retrieval
        });
        
        it("should handle POST requests", () => {
            // Test data creation
        });
        
        it("should handle error responses", () => {
            // Test error scenarios
        });
    });

    describe("Permission Tests", () => {
        it("should allow admin access", () => {
            // Test admin permissions
        });
        
        it("should restrict user access", () => {
            // Test user limitations
        });
    });
    
    afterEach(() => {
        // Cleanup after each test
    });
});

By following this comprehensive testing guide, you'll ensure that ChurchCRM maintains high code quality and reliability while making the testing process efficient and maintainable for all contributors.

Clone this wiki locally