ScreenshotNeo

BlogHow-to

Headless Website Testing with Mocha

Run reliable headless website tests with Mocha, Puppeteer or Playwright. Includes browser setup, CI configuration, debugging and ScreenshotNeo screenshots.

By the ScreenshotNeo team1 October 20267 min read

What “headless website testing with Mocha” means

Mocha is the test runner and assertion orchestration layer. It does not launch a browser by itself. For a Node.js end-to-end test, combine Mocha with a browser-control library such as Puppeteer or Playwright; the library starts Chromium or another supported browser, drives pages, and exposes observations that Mocha asserts. A second pattern loads Mocha’s browser build inside a test page, where the browser executes the test scripts directly.

This guide shows both patterns, then builds a CI-ready Puppeteer example. The current Mocha getting-started guide (v12.0.0) requires Node.js ^20.19.0 || >=22.12.0; verify your runtime before installing.

Choose an architecture

Pattern Browser control Best for Trade-offs
Mocha in a browser page The page itself; no Node automation API Browser-side modules and in-browser reports Requires a served HTML harness; CI needs a way to open the page and collect results
Mocha under Node.js Puppeteer or Playwright launches and controls a headless browser End-to-end flows, navigation, clicks, network assertions and CI Browser binaries and OS dependencies must be installed and pinned

The Node-driven flow is: start your application, invoke Mocha, launch a browser, navigate and interact, then assert results and report failures.

Option A: run Mocha in a browser page

Mocha publishes browser builds. Load mocha.js and its CSS, configure an interface with mocha.setup(), load test scripts, then call mocha.run(). Browser options use camelCase, such as checkLeaks.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css">
</head>
<body>
  <div id="mocha"></div>
  <script src="https://unpkg.com/chai@4/chai.js"></script>
  <script src="https://unpkg.com/mocha/mocha.js"></script>
  <script>
    mocha.setup({ ui: "bdd", timeout: 5000, checkLeaks: true, reporter: "spec" });
    const { expect } = chai;
  </script>
  <script src="./site.test.js"></script>
  <script>mocha.run();</script>
</body>
</html>
// site.test.js
describe("home page", function () {
  it("has a primary heading", async function () {
    const response = await fetch("/");
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const html = await response.text();
    expect(html).to.include("<h1");
  });
});

Serve these files over HTTP rather than opening them with file://. Use ?grep=home on the harness URL to filter tests. The HTML reporter is the default in a browser; use the console when diagnosing failures.

Option B: Mocha with Puppeteer

Puppeteer provides a high-level API for Chrome or Firefox and runs headless by default. Its package normally downloads a compatible Chrome during installation; puppeteer-core omits that download and expects you to provide an executable.

1. Create the project

mkdir mocha-headless-demo
cd mocha-headless-demo
npm init -y
npm i -D mocha@12 puppeteer@latest
mkdir test

Mocha v12 requires Node.js ^20.19.0 || >=22.12.0. Pin versions in your lockfile and CI image. If install scripts are blocked, run npx puppeteer browsers install.

2. Add a test script

{
  "type": "module",
  "scripts": { "test:e2e": "mocha --timeout 30000 --exit" }
}

3. Write the test

// test/home.test.js
import assert from "node:assert/strict";
import puppeteer from "puppeteer";

const baseUrl = process.env.BASE_URL || "http://127.0.0.1:3000";

describe("home page", function () {
  this.timeout(30000);
  let browser, page;

  before(async function () {
    browser = await puppeteer.launch({ headless: true });
    page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
    page.on("console", message => {
      if (message.type() === "error") console.error("[browser]", message.text());
    });
    page.on("pageerror", error => console.error("[pageerror]", error.message));
    page.on("requestfailed", request => console.error("[requestfailed]", request.url(), request.failure()?.errorText));
  });

  after(async function () { await browser?.close(); });

  it("loads successfully and exposes a heading", async function () {
    const response = await page.goto(baseUrl, { waitUntil: "networkidle2", timeout: 30000 });
    assert.ok(response);
    assert.equal(response.status(), 200);
    await page.waitForSelector("h1", { timeout: 10000 });
    const heading = await page.$eval("h1", el => el.textContent.trim());
    assert.ok(heading.length > 0);
  });

  it("completes the sign-in flow", async function () {
    await page.goto(`${baseUrl}/login`, { waitUntil: "domcontentloaded" });
    await page.type("#email", "test@example.invalid");
    await page.type("#password", process.env.TEST_PASSWORD);
    await page.click("button[type=submit]");
    await page.waitForNavigation({ waitUntil: "networkidle2" });
    await page.waitForSelector("[data-testid=account]");
  });
});

Keep credentials in CI secrets and use a dedicated test account. Prefer stable attributes such as data-testid over styling classes.

4. Start the server and run Mocha

# terminal 1
npm run start:test

# terminal 2
BASE_URL=http://127.0.0.1:3000 npm run test:e2e

In CI, start the server as a managed process and poll a health endpoint until it responds. Avoid fixed sleeps.

Playwright as the browser-control alternative

Playwright can occupy the same control layer while Mocha remains the runner. Its browser documentation distinguishes a headless shell from newer Chromium headless modes and documents branded Chrome and Edge channels. Choose the mode that matches the production target.

npm i -D mocha@12 playwright@latest
npx playwright install chromium
// test/playwright-home.test.js
import assert from "node:assert/strict";
import { chromium } from "playwright";

describe("home page", function () {
  this.timeout(30000);
  let browser, page;
  before(async () => {
    browser = await chromium.launch({ headless: true });
    page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
  });
  after(async () => { await browser?.close(); });
  it("renders a heading", async () => {
    const response = await page.goto(process.env.BASE_URL || "http://127.0.0.1:3000", { waitUntil: "networkidle", timeout: 30000 });
    assert.equal(response.status(), 200);
    await page.locator("h1").waitFor();
    assert.ok((await page.locator("h1").innerText()).trim());
  });
});

Puppeteer and Playwright are alternatives for browser control, not replacements for Mocha. Compare browser coverage, browser fidelity, headless implementation, install footprint, diagnostics and fixture reuse.

Configuration for deterministic tests

Concern Recommendation Reason
Viewport Set width, height and device scale factor Prevents responsive breakpoints changing
Navigation Use bounded navigation waits plus selectors Network idle may never occur on analytics-heavy pages
Timeouts Set suite and selector timeouts Failures surface instead of hanging
Data Seed deterministic records Removes dependence on production state
Network Stub irrelevant third-party calls Reduces flakiness and runtime
Browser Pin Node, library, browser and OS image Rendering and protocol behavior are version-sensitive

Use Mocha hooks for setup and cleanup. Avoid shared mutable state. Use --grep for focused runs and --bail while debugging. In browser Mocha, convert dashed options to camelCase.

CI checklist

  1. Use a Node image meeting Mocha’s required version range.
  2. Install dependencies from the lockfile.
  3. Install browser binaries explicitly.
  4. Install Linux browser libraries required by the runner.
  5. Start the app and wait for a health URL.
  6. Pass BASE_URL and credentials through CI secrets.
  7. Run Mocha with bounded timeouts and a CI-compatible reporter.
  8. On failure, save a screenshot, HTML, console messages, page errors and failed requests.
  9. Close browsers in after hooks.
  10. Retry only known transient boundaries.

Debugging and troubleshooting

Symptom Cause Fix
mocha: command not found Mocha is missing or local bin is not used Install with npm i -D mocha and run npx mocha.
Unsupported Node.js version Mocha v12 requirement is unmet Upgrade to ^20.19.0 || >=22.12.0.
Could not find Chrome Install script was blocked or core package has no executable Run npx puppeteer browsers install or provide executablePath.
Browser exits in a container Missing libraries or sandbox restrictions Use a browser-ready image and install dependencies.
Navigation timeout Server is not ready or wait condition never completes Poll readiness; use domcontentloaded plus a selector.
Element not found Race condition, wrong frame or changed selector Wait for a stable selector and set the viewport.
Passes locally, fails in CI Different browser, fonts, timezone, locale or data Pin the environment and capture diagnostics.
Mocha never exits Open browser, server, timer or socket Close resources in after; use --exit only as a guard.
No tests in browser harness Wrong script order, path or file URL Call mocha.setup() before tests and serve over HTTP.

Performance, reliability and cost

  • Reuse one browser per suite and isolate pages or contexts per test.
  • Start with serial tests before increasing parallel workers.
  • Replace arbitrary sleeps with bounded selector, response or state waits.
  • Capture artifacts on failure or for a sample to reduce CI storage.
  • Use deterministic fixtures, stable selectors, pinned binaries and controlled locale/timezone.
  • Cache dependencies and browser directories where CI policy permits, invalidating caches when lockfiles or browser revisions change.

Or skip the browser setup

If you need a screenshot artifact rather than interactive assertions, ScreenshotNeo provides a single HTTP request. See the API documentation for options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require("node:fs").writeFileSync("shot.webp", Buffer.from(await res.arrayBuffer()));

Cookie banners, newsletter popups and chat widgets are removed before the shot. Bot checks, blank pages and failed loads are never billed; response headers identify the page verdict and billing status. ScreenshotNeo includes an MCP server with take_screenshot, get_page_info and capture_pdf tools for AI agents. The Free plan includes 1,000 screenshots monthly with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can Mocha launch a headless browser?

No. Mocha runs and reports tests. Add Puppeteer, Playwright or another browser-control layer, or load Mocha’s browser build in a page.

Is Puppeteer required?

No. It is one JavaScript option. Playwright is another.

Should I use networkidle for every page?

No. Apps with analytics, WebSockets or polling may never become idle. Combine a bounded navigation wait with an assertion-specific selector or response.

How do I test a real browser brand?

Run the matching Chrome, Edge or Firefox channel and pin its version.

When is the browser-page Mocha build useful?

Use it when tests need browser globals and an in-page report. Use Node-driven Mocha when CI must control navigation, contexts, files and diagnostics.

Sources