Comprehensive Logging and Reporting in Cypress (Part 3)

Learn how to implement advanced logging and reporting in Cypress to track test execution, debug issues faster, and create professional test reports

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:

  1. A test fails intermittently in the CI pipeline but works on your machine
  2. You need to demonstrate test coverage to non-technical stakeholders
  3. Tests pass, but you want to verify they're testing the right things
  4. 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>{{reportPageTitle}}</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>{{reportPageTitle}}</h1>
    <p>Test run: {{dateTime}}</p>
  </header>
  
  <div class="container">
    <div class="summary">
      <div class="summary-item">
        <div class="summary-label">Total Tests</div>
        <div class="summary-value">{{stats.tests}}</div>
      </div>
      <div class="summary-item">
        <div class="summary-label">Passes</div>
        <div class="summary-value passes">{{stats.passes}}</div>
      </div>
      <div class="summary-item">
        <div class="summary-label">Failures</div>
        <div class="summary-value failures">{{stats.failures}}</div>
      </div>
      <div class="summary-item">
        <div class="summary-label">Duration</div>
        <div class="summary-value">{{formatDuration stats.duration}}</div>
      </div>
    </div>
    
    {{#each suites}}
    <div class="test-suite">
      <div class="suite-header">{{title}}</div>
      
      {{#each tests}}
      <div class="test-case">
        <div class="test-title" onclick="toggleTestDetails(this)">
          <div class="test-status {{state}}"></div>
          <div>{{title}}</div>
          <div class="test-duration">{{formatDuration duration}}</div>
        </div>
        
        <div class="test-details">
          {{#if context}}
          {{#each context}}
          <div class="log-section">
            <div class="log-title">{{title}}</div>
            <div class="log-content">
              {{#if value.message}}
              <div class="log-entry">{{value.message}}</div>
              {{else}}
              <pre>{{json value}}</pre>
              {{/if}}
            </div>
          </div>
          {{/each}}
          {{/if}}
          
          {{#if err}}
          <div class="log-section">
            <div class="log-title">Error</div>
            <div class="error-message">{{err.message}}</div>
          </div>
          {{/if}}
          
          {{#if screenshots}}
          <div class="log-section">
            <div class="log-title">Screenshots</div>
            {{#each screenshots}}
            <div>
              <p>{{this.name}}</p>
              <img src="{{this.path}}" class="screenshot" alt="{{this.name}}">
            </div>
            {{/each}}
          </div>
          {{/if}}
          
          {{#if video}}
          <div class="log-section">
            <div class="log-title">Video</div>
            <div class="video-container">
              <video controls class="video-player">
                <source src="{{video}}" type="video/mp4">
                Your browser does not support video playback.
              </video>
            </div>
          </div>
          {{/if}}
        </div>
      </div>
      {{/each}}
    </div>
    {{/each}}
  </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:

  1. Log Strategically: Don't log every action; focus on key steps and verification points.

  2. Use Clear Log Messages: Write clear, descriptive messages that explain what's happening.

  3. Structure Test Data: Use consistent formats for test data to make reports easier to parse.

  4. Add Context to Failures: When assertions fail, include context that helps understand why.

  5. Clean Up Old Reports: Implement a system to remove old reports to save disk space.

  6. Set Up Alert Notifications: Configure your CI system to notify the team when tests fail.

  7. 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:

  1. Getting Started with Cypress: Setup and First Test
  2. Building Maintainable Tests with Page Object Model
  3. 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!

Enjoying this?