Headless Website Testing with Mocha
Run reliable headless website tests with Mocha, Puppeteer or Playwright. Includes browser setup, CI configuration, debugging and ScreenshotNeo screenshots.
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
- Use a Node image meeting Mocha’s required version range.
- Install dependencies from the lockfile.
- Install browser binaries explicitly.
- Install Linux browser libraries required by the runner.
- Start the app and wait for a health URL.
- Pass
BASE_URLand credentials through CI secrets. - Run Mocha with bounded timeouts and a CI-compatible reporter.
- On failure, save a screenshot, HTML, console messages, page errors and failed requests.
- Close browsers in
afterhooks. - 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.


