BLOG POSTS
    MangoHost Blog / Writing End-to-End Tests in Node.js with Puppeteer and Jest
Writing End-to-End Tests in Node.js with Puppeteer and Jest

Writing End-to-End Tests in Node.js with Puppeteer and Jest

End-to-end testing in Node.js has become essential for ensuring your web applications work correctly from the user’s perspective, and the combination of Puppeteer and Jest provides one of the most robust testing environments available. Puppeteer’s Chrome automation capabilities paired with Jest’s testing framework create a powerful setup for testing complex user interactions, form submissions, navigation flows, and visual elements. This guide will walk you through setting up a complete E2E testing environment, implementing real-world test scenarios, and avoiding common pitfalls that can derail your testing efforts.

How End-to-End Testing Works with Puppeteer and Jest

Puppeteer operates by launching a headless Chrome browser instance and controlling it through the Chrome DevTools Protocol. Unlike unit tests that check individual functions or integration tests that verify component interactions, E2E tests simulate actual user behavior by clicking buttons, filling forms, and navigating pages just like a real person would.

Jest acts as the test runner and assertion library, providing structure for your test suites and methods for verifying expected outcomes. When combined, Puppeteer handles browser automation while Jest manages test execution, reporting, and organization.

The typical flow looks like this: Jest launches your test suite, Puppeteer spins up a Chrome instance, navigates to your application, performs user actions, captures results, and Jest evaluates whether the test passed or failed based on your assertions.

Step-by-Step Implementation Guide

First, install the necessary dependencies in your Node.js project:

npm install --save-dev puppeteer jest
npm install --save-dev jest-puppeteer

Create a Jest configuration file called jest.config.js in your project root:

module.exports = {
  preset: 'jest-puppeteer',
  testMatch: ['**/tests/**/*.test.js'],
  testTimeout: 30000,
  setupFilesAfterEnv: ['/jest.setup.js']
};

Set up your Jest environment configuration in jest-puppeteer.config.js:

module.exports = {
  launch: {
    headless: true,
    slowMo: 50,
    devtools: false,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-dev-shm-usage',
      '--disable-web-security'
    ]
  },
  server: {
    command: 'npm start',
    port: 3000,
    launchTimeout: 10000,
    debug: true
  }
};

Create a setup file jest.setup.js for global configurations:

jest.setTimeout(30000);

beforeEach(async () => {
  await page.setViewport({ width: 1366, height: 768 });
  await page.setUserAgent('Mozilla/5.0 (compatible; Jest-Puppeteer)');
});

Now create your first test file in the tests directory:

// tests/login.test.js
describe('User Authentication', () => {
  beforeAll(async () => {
    await page.goto('http://localhost:3000');
  });

  test('should display login form', async () => {
    await expect(page).toMatchElement('form#login-form');
    await expect(page).toMatchElement('input[name="email"]');
    await expect(page).toMatchElement('input[name="password"]');
  });

  test('should login with valid credentials', async () => {
    await page.type('input[name="email"]', 'user@example.com');
    await page.type('input[name="password"]', 'password123');
    
    await Promise.all([
      page.waitForNavigation(),
      page.click('button[type="submit"]')
    ]);
    
    await expect(page.url()).toMatch(/dashboard/);
    await expect(page).toMatchElement('.welcome-message');
  });

  test('should show error for invalid credentials', async () => {
    await page.goto('http://localhost:3000/login');
    await page.type('input[name="email"]', 'invalid@example.com');
    await page.type('input[name="password"]', 'wrongpassword');
    
    await page.click('button[type="submit"]');
    await page.waitForSelector('.error-message');
    
    const errorText = await page.$eval('.error-message', el => el.textContent);
    expect(errorText).toContain('Invalid credentials');
  });
});

Real-World Examples and Use Cases

Here’s a comprehensive example testing an e-commerce checkout process:

// tests/checkout.test.js
describe('E-commerce Checkout Flow', () => {
  test('complete purchase workflow', async () => {
    // Navigate to product page
    await page.goto('http://localhost:3000/products/123');
    
    // Add to cart
    await page.click('.add-to-cart-btn');
    await page.waitForSelector('.cart-notification');
    
    // Verify cart count
    const cartCount = await page.$eval('.cart-count', el => el.textContent);
    expect(cartCount).toBe('1');
    
    // Go to checkout
    await page.click('.cart-icon');
    await page.waitForSelector('.checkout-form');
    
    // Fill shipping information
    await page.type('input[name="firstName"]', 'John');
    await page.type('input[name="lastName"]', 'Doe');
    await page.type('input[name="address"]', '123 Main St');
    await page.type('input[name="city"]', 'New York');
    await page.select('select[name="state"]', 'NY');
    await page.type('input[name="zipCode"]', '10001');
    
    // Fill payment information
    await page.type('input[name="cardNumber"]', '4111111111111111');
    await page.type('input[name="expiryDate"]', '12/25');
    await page.type('input[name="cvv"]', '123');
    
    // Submit order
    await Promise.all([
      page.waitForNavigation(),
      page.click('.place-order-btn')
    ]);
    
    // Verify success page
    await expect(page).toMatchElement('.order-confirmation');
    const orderNumber = await page.$eval('.order-number', el => el.textContent);
    expect(orderNumber).toMatch(/ORD-\d{6}/);
  });
});

Testing file uploads and form interactions:

// tests/file-upload.test.js
describe('File Upload Functionality', () => {
  test('should upload and process image file', async () => {
    await page.goto('http://localhost:3000/upload');
    
    // Upload file
    const fileInput = await page.$('input[type="file"]');
    await fileInput.uploadFile('./test-assets/sample-image.jpg');
    
    // Wait for upload progress
    await page.waitForSelector('.upload-progress');
    await page.waitForSelector('.upload-complete', { timeout: 10000 });
    
    // Verify upload success
    const uploadedImage = await page.$('.uploaded-image');
    expect(uploadedImage).toBeTruthy();
    
    const imageUrl = await page.$eval('.uploaded-image', img => img.src);
    expect(imageUrl).toMatch(/uploads\/.*\.jpg$/);
  });
});

Comparison with Alternative Testing Tools

Feature Puppeteer + Jest Cypress Selenium WebDriver Playwright
Browser Support Chrome/Chromium only Chrome, Firefox, Edge All major browsers Chrome, Firefox, Safari, Edge
Setup Complexity Medium Low High Medium
Execution Speed Fast Medium Slow Fast
Debugging Tools Good Excellent Limited Good
Community Support Large Large Very Large Growing
Performance Overhead Low Medium High Low

Advanced Testing Patterns and Best Practices

Implement Page Object Model for maintainable tests:

// pages/LoginPage.js
class LoginPage {
  constructor(page) {
    this.page = page;
    this.emailInput = 'input[name="email"]';
    this.passwordInput = 'input[name="password"]';
    this.submitButton = 'button[type="submit"]';
    this.errorMessage = '.error-message';
  }

  async navigate() {
    await this.page.goto('http://localhost:3000/login');
  }

  async login(email, password) {
    await this.page.type(this.emailInput, email);
    await this.page.type(this.passwordInput, password);
    await Promise.all([
      this.page.waitForNavigation(),
      this.page.click(this.submitButton)
    ]);
  }

  async getErrorMessage() {
    return await this.page.$eval(this.errorMessage, el => el.textContent);
  }
}

module.exports = LoginPage;

Use the Page Object in your tests:

// tests/login-pom.test.js
const LoginPage = require('../pages/LoginPage');

describe('Login with Page Object Model', () => {
  let loginPage;

  beforeEach(async () => {
    loginPage = new LoginPage(page);
    await loginPage.navigate();
  });

  test('should login successfully', async () => {
    await loginPage.login('user@example.com', 'password123');
    expect(page.url()).toMatch(/dashboard/);
  });
});

Implement custom matchers for better assertions:

// jest.setup.js
expect.extend({
  async toHaveText(elementHandle, expectedText) {
    const actualText = await elementHandle.evaluate(el => el.textContent);
    const pass = actualText.includes(expectedText);
    
    return {
      message: () => `expected element to contain "${expectedText}" but got "${actualText}"`,
      pass
    };
  }
});

Performance Optimization and Monitoring

Monitor and measure performance during testing:

// tests/performance.test.js
describe('Performance Testing', () => {
  test('page load performance', async () => {
    const startTime = Date.now();
    
    await page.goto('http://localhost:3000', { waitUntil: 'networkidle0' });
    
    const loadTime = Date.now() - startTime;
    expect(loadTime).toBeLessThan(3000); // 3 seconds max
    
    // Check Core Web Vitals
    const metrics = await page.metrics();
    console.log('Performance metrics:', metrics);
    
    // Lighthouse performance audit
    const lighthouseMetrics = await page.evaluate(() => {
      return new Promise((resolve) => {
        new PerformanceObserver((list) => {
          const entries = list.getEntries();
          resolve(entries);
        }).observe({ entryTypes: ['navigation'] });
      });
    });
    
    expect(lighthouseMetrics).toBeDefined();
  });
});

Common Pitfalls and Troubleshooting

Handle timing issues with proper waits:

// Bad - race condition prone
await page.click('.submit-btn');
const result = await page.$eval('.result', el => el.textContent);

// Good - wait for element to appear
await page.click('.submit-btn');
await page.waitForSelector('.result');
const result = await page.$eval('.result', el => el.textContent);

// Better - wait for specific condition
await page.click('.submit-btn');
await page.waitForFunction(
  () => document.querySelector('.result')?.textContent?.length > 0,
  { timeout: 5000 }
);
const result = await page.$eval('.result', el => el.textContent);

Handle dynamic content and AJAX requests:

// Wait for network activity to complete
await page.goto('http://localhost:3000', { waitUntil: 'networkidle0' });

// Wait for specific network request
await page.setRequestInterception(true);
page.on('request', request => {
  if (request.url().includes('/api/data')) {
    console.log('API request detected');
  }
  request.continue();
});

// Wait for response
await Promise.all([
  page.waitForResponse(response => 
    response.url().includes('/api/data') && response.status() === 200
  ),
  page.click('.load-data-btn')
]);

Memory management and resource cleanup:

// Global teardown in jest-puppeteer.config.js
module.exports = {
  launch: {
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  },
  teardown: './teardown.js'
};

// teardown.js
module.exports = async () => {
  if (global.__BROWSER__) {
    await global.__BROWSER__.close();
  }
};

CI/CD Integration and Docker Setup

Configure tests for continuous integration environments:

# Dockerfile for testing environment
FROM node:16-alpine

RUN apk add --no-cache \
    chromium \
    nss \
    freetype \
    freetype-dev \
    harfbuzz \
    ca-certificates \
    ttf-freefont

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .

CMD ["npm", "test"]

GitHub Actions workflow example:

# .github/workflows/e2e-tests.yml
name: E2E Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run E2E tests
        run: npm run test:e2e
        env:
          CI: true

For production deployments requiring robust server infrastructure, consider using VPS solutions or dedicated servers that provide consistent performance for running automated test suites.

Key resources for further learning include the official Puppeteer documentation and Jest’s Puppeteer integration guide. The combination of these tools creates a powerful testing environment that can handle complex user scenarios while maintaining fast execution times and reliable results across different environments.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked