-
Notifications
You must be signed in to change notification settings - Fork 506
Testing
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.
- ✅ 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
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
- Docker development environment set up and running
- ChurchCRM application accessible at
http://localhost/ - Node.js 20+ installed
# 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# 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 debuggingWhen 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 |
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
/// <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");
});
});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");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");
});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");
});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();
});-
Prefer data attributes: Use
data-cyordata-testidattributescy.get("[data-cy=submit-button]").click();
-
Use semantic selectors: Target elements by their semantic meaning
cy.contains("Save Changes").click(); cy.get("button[type=submit]").click();
-
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();
/// <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');
});
});
});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);
});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);
});
});
});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);
});
});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');
}
});
});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);
});
});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);
}
});
});Your PR will NOT be merged unless:
- ✅ All tests pass - Run
npm run testbefore 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
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
Every new feature must include tests for:
- UI Tests: Cover all user interactions and edge cases
- API Tests: Validate all endpoints and data operations
- Permission Tests: Verify access controls work correctly
- Error Handling: Test validation and error scenarios
- Integration Tests: Ensure feature works with existing functionality
// 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);
});
});
});- 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
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");
});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");
});it("should download reports", () => {
cy.loginAdmin("Reports.php");
cy.get("#generateReport").click();
cy.verifyDownload(".pdf", { contains: true });
});- 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
- 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
- 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
- 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()
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'
]
}
});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
Tests run automatically on:
- Every pull request
- Commits to main branch
- Scheduled nightly builds
- All tests must pass before merge
- New features require corresponding tests
- Test coverage should not decrease
- Performance tests should not regress
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:stopthennpm run docker:test:startto reset
Flaky tests (intermittent failures):
- Add explicit waits for dynamic content instead of hardcoded
cy.wait() - Use
cy.contains()orcy.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 deprecatedattachFile()
Debugging strategy:
- Run test in interactive mode:
npx cypress open --config-file docker/cypress.config.ts - Pause at failure point:
cy.pause() - Check browser console and command log
- Take screenshot:
cy.screenshot() - Verify element is in DOM using Chrome DevTools
# 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.tsUse 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.
- Installation Guide ← Start here!
- First Run Setup
- Features Overview
- Upgrade Guide
- Backup & Restore
- Rollback Procedures
- File Permissions
- Troubleshooting
- Logging & Diagnostics
- SSL/HTTPS Security
- Localization Overview