In the first two parts of this series, we covered setting up Cypress and implementing the Page Object Model for maintainable tests. Now it's time to focus on a critical aspect of test automation that's often overlooked: comprehensive logging and reporting.
When tests fail in CI/CD pipelines or when run by other team members, detailed logs and visual evidence are invaluable for debugging. In this article, we'll create a robust reporting system that captures everything needed to understand test execution and quickly fix issues.
Why Advanced Reporting Matters
Basic pass/fail reporting is insufficient for real-world testing. Consider these scenarios:
- A test fails intermittently in the CI pipeline but works on your machine
- You need to demonstrate test coverage to non-technical stakeholders
- Tests pass, but you want to verify they're testing the right things
- You're debugging a complex test failure and need to understand the sequence of events
For these situations, you need a comprehensive reporting system that captures:
- Screenshots at critical points or on failure
- Videos of test execution
- Network requests to verify API calls
- Console logs to catch JavaScript errors
- Custom logs for test-specific information
- Performance metrics to identify slow tests
Let's build this system step by step.
Setting Up Mochawesome Reporter
First, we'll set up Mochawesome, a popular reporter that creates beautiful HTML reports:
npm install --save-dev cypress-mochawesome-reporter
Update your cypress.config.js
file:
const { defineConfig } = require('cypress');
module.exports = defineConfig({
reporter: 'cypress-mochawesome-reporter',
reporterOptions: {
charts: true,
reportPageTitle: 'YouTube Search Tests',
embeddedScreenshots: true,
inlineAssets: true,
saveAllAttempts: false,
},
e2e: {
baseUrl: 'https://www.youtube.com',
setupNodeEvents(on, config) {
require('cypress-mochawesome-reporter/plugin')(on);
// We'll add more plugins here later
},
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 10000,
},
});
Next, update your cypress/support/e2e.js
file:
// cypress/support/e2e.js
import './commands';
import 'cypress-mochawesome-reporter/register';
Now run your tests with:
npx cypress run
You'll find HTML reports in the cypress/reports
directory. These reports include basic test information, but we'll enhance them with more detailed data.
Capturing Screenshots
Cypress automatically captures screenshots on test failure when running in headless mode. Let's enhance this with custom screenshots at critical points:
Add this to your cypress/support/commands.js
:
// cypress/support/commands.js
/**
* Take a screenshot with a custom name and add it to the report
* @param {string} name - Descriptive name for the screenshot
*/
Cypress.Commands.add('takeScreenshot', (name) => {
cy.screenshot(name, {
capture: 'viewport',
overwrite: true
});
});
Now you can use this in your tests:
it('should search and verify results', () => {
const searchTerm = 'Cypress test automation';
HomePage.visitHomePage();
cy.takeScreenshot('home-page-loaded');
HomePage.search(searchTerm);
cy.takeScreenshot('after-search-submission');
SearchResultsPage.verifySearchResults(searchTerm);
cy.takeScreenshot('search-results-displayed');
});
You can also capture screenshots conditionally:
SearchResultsPage.getResultsCount().then(count => {
if (count > 10) {
cy.takeScreenshot('many-results-found');
}
});
Recording Videos of Test Execution
Cypress automatically records videos in headless mode. Let's configure video compression and when videos should be saved:
// cypress.config.js
module.exports = defineConfig({
// ... other config
e2e: {
// ... other e2e config
video: true,
videoCompression: 32,
// Only save videos for failed tests to save space
videoUploadOnPasses: false,
},
});
The videoCompression
setting ranges from 0 (no compression) to 100 (max compression). The value 32 offers a good balance between quality and file size.
Capturing Network Logs
Network logs are crucial for debugging API-related issues. Let's set up network logging using cy.intercept()
:
// cypress/support/e2e.js
import './commands';
import 'cypress-mochawesome-reporter/register';
// Set up network logging for all requests
let networkLogs = [];
// Log all XHR requests
Cypress.on('log:added', (log) => {
if (log.displayName === 'xhr' || log.name === 'xhr') {
networkLogs.push({
time: new Date().toISOString(),
requestType: log.displayName,
message: log.message,
url: log.consoleProps?.Stubbed === 'Yes'
? log.consoleProps?.URL
: log.consoleProps?.Request?.url,
method: log.consoleProps?.Method,
status: log.consoleProps?.Status,
duration: log.consoleProps?.Duration
});
}
});
// Reset network logs before each test
beforeEach(() => {
networkLogs = [];
});
// Save network logs as test metadata after each test
afterEach(function() {
// Add network logs to the test metadata
if (networkLogs.length > 0) {
cy.addTestContext({ title: 'Network Logs', value: networkLogs });
}
});
This code captures all XHR requests during test execution and adds them to the test report. You can also set up specific request interception to log more details:
// Specific intercept for search API
cy.intercept('GET', '**/search*').as('searchRequest');
// Search for something
HomePage.search('Cypress test automation');
// Wait for the request and log detailed information
cy.wait('@searchRequest').then(interception => {
cy.addTestContext({
title: 'Search API Response',
value: {
url: interception.request.url,
method: interception.request.method,
requestBody: interception.request.body,
statusCode: interception.response.statusCode,
responseBody: interception.response.body
}
});
});
For this to work, we need to install the cypress-test-context
plugin:
npm install --save-dev cypress-plugin-context
Then update your cypress/support/e2e.js
file:
// cypress/support/e2e.js
import './commands';
import 'cypress-mochawesome-reporter/register';
import 'cypress-plugin-context';
// ... rest of your code
Capturing Console Logs
Browser console logs are essential for catching JavaScript errors. Let's capture these logs:
First, install the required plugin:
npm install --save-dev cypress-plugin-console-log
Then update cypress.config.js
:
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
// ... other config
e2e: {
setupNodeEvents(on, config) {
require('cypress-mochawesome-reporter/plugin')(on);
require('cypress-plugin-console-log/on-plugin')(on);
// More plugins here
},
// ... other e2e config
},
});
And update cypress/support/e2e.js
:
// cypress/support/e2e.js
import './commands';
import 'cypress-mochawesome-reporter/register';
import 'cypress-plugin-context';
import 'cypress-plugin-console-log/on-support';
// Store console logs
let consoleLogs = [];
// Listen for console logs
Cypress.on('console:log', (log) => {
consoleLogs.push({
type: log.type,
message: log.message,
timestamp: new Date().toISOString()
});
});
// Reset console logs before each test
beforeEach(() => {
consoleLogs = [];
});
// Add console logs to test context after each test
afterEach(function() {
if (consoleLogs.length > 0) {
cy.addTestContext({ title: 'Console Logs', value: consoleLogs });
}
});
Creating Custom Logs for Test Steps
For better test documentation, let's create a custom logging function that records each test step:
// cypress/support/commands.js
/**
* Log a test step with optional metadata
* @param {string} message - Description of the step
* @param {Object} metadata - Optional metadata to include
*/
Cypress.Commands.add('logStep', (message, metadata = {}) => {
Cypress.log({
name: '🔍 STEP',
message: message,
consoleProps: () => metadata
});
cy.addTestContext({
title: 'Step',
value: {
message,
...metadata,
timestamp: new Date().toISOString()
}
});
});
Now we can use this in our tests:
it('should search for videos and filter results', () => {
// Visit homepage
cy.logStep('Navigating to YouTube homepage');
HomePage.visitHomePage();
// Perform search
const searchTerm = 'Cypress test automation';
cy.logStep(`Searching for "${searchTerm}"`);
HomePage.search(searchTerm);
// Verify results
cy.logStep('Verifying search results');
SearchResultsPage.verifySearchResults(searchTerm);
SearchResultsPage.verifyMinimumResultsCount(5);
// Apply filter
cy.logStep('Applying "This month" filter');
SearchResultsPage.filterByThisMonth();
// Verify filtered results
cy.logStep('Verifying filtered results');
SearchResultsPage.shouldBeVisible(SearchResultsPage.selectors.searchResults);
});
Integrating with Our Page Object Model
Now let's integrate our logging system with the page objects we created in Part 2. Here's how we can enhance the BasePage
class:
// cypress/support/page-objects/BasePage.js
class BasePage {
// ... existing methods
/**
* Navigate to a specific URL with logging
* @param {string} url - The URL to navigate to
*/
navigate(url) {
cy.logStep(`Navigating to: ${url}`);
cy.visit(url);
}
/**
* Click an element with logging
* @param {string} selector - CSS selector
* @param {string} description - Optional description of the element
*/
click(selector, description = selector) {
cy.logStep(`Clicking on: ${description}`);
this.getElement(selector).click();
}
/**
* Type text into an input field with logging
* @param {string} selector - CSS selector
* @param {string} text - Text to type
* @param {string} description - Optional description of the field
*/
type(selector, text, description = selector) {
cy.logStep(`Typing "${text}" into: ${description}`);
this.getElement(selector).type(text);
}
/**
* Add a verification step with screenshot
* @param {string} message - Description of what's being verified
*/
logVerification(message) {
cy.logStep(`Verifying: ${message}`);
// Take a screenshot for this verification point
cy.takeScreenshot(`verify-${message.toLowerCase().replace(/\s+/g, '-')}`);
}
}
export default BasePage;
Now update our page objects to use these enhanced methods:
// cypress/support/page-objects/HomePage.js
// ... imports
class HomePage extends BasePage {
// ... selectors
/**
* Search for a specific term with enhanced logging
* @param {string} searchTerm - Term to search for
*/
search(searchTerm) {
this.type(this.selectors.searchInput, searchTerm, 'Search field');
this.click(this.selectors.searchButton, 'Search button');
cy.takeScreenshot(`after-search-for-${searchTerm.replace(/\s+/g, '-')}`);
}
// ... other methods with enhanced logging
}
// ... export
Capturing Performance Metrics
Let's add performance metrics to our reports to identify slow tests:
// cypress/support/e2e.js
// At the beginning of the file
const startTimes = {};
// Before each test
beforeEach(function() {
// ... existing code
startTimes[this.currentTest.title] = Date.now();
});
// After each test
afterEach(function() {
// ... existing code
// Calculate duration
const testTitle = this.currentTest.title;
const duration = Date.now() - startTimes[testTitle];
// Add performance metrics to test context
cy.addTestContext({
title: 'Performance',
value: {
testName: testTitle,
durationMs: duration,
status: this.currentTest.state
}
});
});
Putting It All Together: Complete Reporting Setup
Now, let's create a comprehensive test that utilizes all our logging features:
// cypress/e2e/youtube-search-with-reporting.cy.js
import HomePage from '../support/page-objects/HomePage';
import SearchResultsPage from '../support/page-objects/SearchResultsPage';
import VideoPage from '../support/page-objects/VideoPage';
describe('YouTube Search with Comprehensive Reporting', () => {
beforeEach(() => {
HomePage.visitHomePage();
});
it('should perform a complete search flow with detailed logging', () => {
const searchTerm = 'Cypress test automation';
// Log test parameters
cy.addTestContext({
title: 'Test Parameters',
value: { searchTerm }
});
// 1. Perform search
HomePage.search(searchTerm);
// 2. Verify search results
SearchResultsPage.logVerification('Search results are displayed');
SearchResultsPage.verifySearchResults(searchTerm);
// Log number of results found
SearchResultsPage.getResultsCount().then(count => {
cy.addTestContext({
title: 'Search Results',
value: { count, searchTerm }
});
});
// 3. Apply filter
cy.logStep('Applying "This month" filter');
// Intercept filter request
cy.intercept('GET', '**/search*').as('filterRequest');
SearchResultsPage.filterByThisMonth();
// Wait for filter request and log details
cy.wait('@filterRequest').then(interception => {
cy.addTestContext({
title: 'Filter Request',
value: {
url: interception.request.url,
statusCode: interception.response.statusCode
}
});
});
// 4. Verify filtered results
SearchResultsPage.logVerification('Filtered results are displayed');
SearchResultsPage.verifyMinimumResultsCount(1);
// 5. Click on first result
let firstResultTitle;
SearchResultsPage.getResultText(0).then(text => {
firstResultTitle = text.trim();
cy.addTestContext({
title: 'Selected Video',
value: { title: firstResultTitle }
});
SearchResultsPage.clickResult(0);
});
// 6. Verify video page
VideoPage.logVerification('Video page loaded correctly');
VideoPage.verifyVideoPageLoaded();
// 7. Verify video title matches search result
VideoPage.getVideoTitle().then(videoTitle => {
const titleMatches = videoTitle.includes(firstResultTitle);
cy.addTestContext({
title: 'Title Verification',
value: {
expectedToContain: firstResultTitle,
actual: videoTitle,
matches: titleMatches
}
});
expect(titleMatches).to.be.true;
});
// 8. Capture final state
cy.takeScreenshot('test-complete');
});
});
Creating a Custom HTML Reporter
The default Mochawesome reporter is good, but we can enhance it to better display our custom logs. Let's create a custom reporter template:
First, install the required packages:
npm install --save-dev mochawesome-report-generator handlebars lodash
Create a custom template:
mkdir -p cypress/templates
touch cypress/templates/custom-template.hbs
Add this content to custom-template.hbs
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style>
/* Add your custom CSS styles here */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
header {
background-color: #333;
color: white;
padding: 1em;
text-align: center;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 1em;
}
.summary {
display: flex;
justify-content: space-between;
background-color: white;
padding: 1em;
border-radius: 5px;
margin-bottom: 1em;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.summary-item {
text-align: center;
}
.summary-label {
font-size: 0.8em;
color: #666;
}
.summary-value {
font-size: 1.5em;
font-weight: bold;
}
.summary-value.passes {
color: #26a65b;
}
.summary-value.failures {
color: #e74c3c;
}
.test-suite {
background-color: white;
border-radius: 5px;
margin-bottom: 1em;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.suite-header {
background-color: #eee;
padding: 1em;
font-weight: bold;
border-bottom: 1px solid #ddd;
}
.test-case {
padding: 1em;
border-bottom: 1px solid #eee;
}
.test-case:last-child {
border-bottom: none;
}
.test-title {
display: flex;
align-items: center;
cursor: pointer;
}
.test-title:hover {
color: #3498db;
}
.test-status {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 0.5em;
}
.test-status.pass {
background-color: #26a65b;
}
.test-status.fail {
background-color: #e74c3c;
}
.test-duration {
margin-left: auto;
color: #666;
font-size: 0.8em;
}
.test-details {
margin-top: 1em;
display: none;
}
.test-details.visible {
display: block;
}
.log-section {
margin-bottom: 1em;
}
.log-title {
font-weight: bold;
margin-bottom: 0.5em;
padding-bottom: 0.2em;
border-bottom: 1px solid #eee;
}
.log-entry {
padding: 0.5em;
background-color: #f9f9f9;
border-left: 3px solid #ddd;
margin-bottom: 0.2em;
}
.screenshot {
max-width: 100%;
margin-top: 1em;
border: 1px solid #ddd;
}
.network-log {
font-family: monospace;
font-size: 0.8em;
overflow-x: auto;
}
.network-table {
width: 100%;
border-collapse: collapse;
}
.network-table th, .network-table td {
text-align: left;
padding: 0.5em;
border-bottom: 1px solid #eee;
}
.network-table th {
background-color: #f5f5f5;
}
.video-container {
margin-top: 1em;
}
.video-player {
width: 100%;
max-width: 800px;
}
.error-message {
color: #e74c3c;
background-color: #fadbd8;
padding: 1em;
border-left: 3px solid #e74c3c;
white-space: pre-wrap;
overflow-x: auto;
font-family: monospace;
}
</style>
</head>
<body>
<header>
<h1></h1>
<p>Test run: </p>
</header>
<div class="container">
<div class="summary">
<div class="summary-item">
<div class="summary-label">Total Tests</div>
<div class="summary-value"></div>
</div>
<div class="summary-item">
<div class="summary-label">Passes</div>
<div class="summary-value passes"></div>
</div>
<div class="summary-item">
<div class="summary-label">Failures</div>
<div class="summary-value failures"></div>
</div>
<div class="summary-item">
<div class="summary-label">Duration</div>
<div class="summary-value"></div>
</div>
</div>
<div class="test-suite">
<div class="suite-header"></div>
<div class="test-case">
<div class="test-title" onclick="toggleTestDetails(this)">
<div class="test-status "></div>
<div></div>
<div class="test-duration"></div>
</div>
<div class="test-details">
<div class="log-section">
<div class="log-title"></div>
<div class="log-content">
<div class="log-entry"></div>
<pre></pre>
</div>
</div>
<div class="log-section">
<div class="log-title">Error</div>
<div class="error-message"></div>
</div>
<div class="log-section">
<div class="log-title">Screenshots</div>
<div>
<p></p>
<img src="" class="screenshot" alt="">
</div>
</div>
<div class="log-section">
<div class="log-title">Video</div>
<div class="video-container">
<video controls class="video-player">
<source src="" type="video/mp4">
Your browser does not support video playback.
</video>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function toggleTestDetails(element) {
const details = element.nextElementSibling;
details.classList.toggle('visible');
}
// Auto-expand failed tests
document.addEventListener('DOMContentLoaded', function() {
const failedTests = document.querySelectorAll('.test-status.fail');
failedTests.forEach(function(test) {
const title = test.closest('.test-title');
title.click();
});
});
</script>
</body>
</html>
Now update your cypress.config.js
to use this template:
// cypress.config.js
const { defineConfig } = require('cypress');
const path = require('path');
module.exports = defineConfig({
reporter: 'cypress-mochawesome-reporter',
reporterOptions: {
charts: true,
reportPageTitle: 'YouTube Search Tests',
embeddedScreenshots: true,
inlineAssets: true,
saveAllAttempts: false,
reportDir: 'cypress/reports',
overwrite: false,
html: true,
json: true,
// Use custom template
reportFilename: 'cypress-report-[datetime]',
timestamp: 'yyyy-mm-dd_HH-MM-ss',
// Uncomment this when you've created your custom template
// templatePath: path.join(__dirname, 'cypress/templates/custom-template.hbs')
},
// ... rest of your config
});
Setting Up CI Integration
Finally, let's configure our tests to run in a CI environment. Create a .github/workflows/cypress.yml
file if you're using GitHub Actions:
name: Cypress Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
cypress-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run Cypress tests
uses: cypress-io/github-action@v4
with:
browser: chrome
headless: true
- name: Upload screenshots
uses: actions/upload-artifact@v2
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots
- name: Upload videos
uses: actions/upload-artifact@v2
if: always()
with:
name: cypress-videos
path: cypress/videos
- name: Upload reports
uses: actions/upload-artifact@v2
if: always()
with:
name: cypress-reports
path: cypress/reports
Best Practices for Logging and Reporting
To make the most of your logging and reporting system:
-
Log Strategically: Don't log every action; focus on key steps and verification points.
-
Use Clear Log Messages: Write clear, descriptive messages that explain what's happening.
-
Structure Test Data: Use consistent formats for test data to make reports easier to parse.
-
Add Context to Failures: When assertions fail, include context that helps understand why.
-
Clean Up Old Reports: Implement a system to remove old reports to save disk space.
-
Set Up Alert Notifications: Configure your CI system to notify the team when tests fail.
-
Regular Review: Periodically review test reports to identify flaky tests or performance issues.
Conclusion
Comprehensive logging and reporting are essential for maintaining a robust Cypress test suite. With the system we've built in this article, you'll have detailed information about every test run, making it easier to:
- Debug test failures quickly
- Share test results with stakeholders
- Monitor test performance over time
- Maintain confidence in your test suite
The techniques we've covered—capturing screenshots, videos, network requests, console logs, custom logs, and performance metrics—provide a complete picture of test execution. Combined with the structured Page Object Model from Part 2, you now have a powerful, maintainable, and informative testing framework.
By following these practices, you'll spend less time debugging test failures and more time building features, confident that your automated tests have your back.
This concludes our three-part series on Cypress testing. We've covered:
- Getting Started with Cypress: Setup and First Test
- Building Maintainable Tests with Page Object Model
- Comprehensive Logging and Reporting (this article)
With these fundamentals in place, you're well-equipped to build a robust test automation framework for your web applications. Happy testing!