QUnit has been the backbone of jQuery's test suite for over a decade, and it powers test pipelines in countless JavaScript projects. But QUnit only reports what it sees when it runs. If the CI job that triggers your suite gets accidentally disabled, QUnit produces zero output and zero failures — because it never ran.
This is the silent suite-failure problem. HTTP monitors can't help — there's nothing to ping. The fix is heartbeat monitoring: your QUnit run pings a unique URL after every successful execution, and if Vigilmon doesn't receive that ping within the expected window, you get alerted.
This tutorial shows you how to instrument QUnit test pipelines with Vigilmon heartbeat monitors so you know when tests stop running, not just when they fail.
What You'll Cover
- Heartbeat monitoring for QUnit test suites
- Detecting suites that stop running entirely
- Using QUnit's
donecallback to send a heartbeat on success - Multi-module test monitoring
- Integrating with CI/CD pipelines (GitHub Actions, GitLab CI)
Prerequisites
- A Node.js project with QUnit configured (or a browser-based QUnit setup)
- A free account at vigilmon.online
The Problem: What QUnit's Built-In Reporting Misses
QUnit gives you pass/fail counts, assertion details, and per-module results — but only when it runs. It won't alert you when:
- A CI path filter silently excludes your test files
- A
requireorimporterror causes the test file to bail before any test is registered - A scheduled test job is disabled or misconfigured
- QUnit hangs waiting for an async
assert.async()that never resolves - A build step that generates test fixtures silently fails upstream
Vigilmon's heartbeat pattern closes these gaps by requiring an explicit "I finished successfully" signal from QUnit's own completion hook.
Step 1: Create a Heartbeat Monitor in Vigilmon
- Log in to Vigilmon and click New Monitor → Heartbeat.
- Name it something specific:
QUnit Unit Tests — mainorQUnit Browser Suite — nightly. - Set the Expected interval to match your test schedule. For suites that run on every push, use 2 hours. For nightly suites, use 25 hours.
- Set the Grace period to 30 minutes for push-triggered suites, or 1 hour for scheduled suites.
- Save and copy the Ping URL — it looks like
https://vigilmon.online/api/heartbeat/<unique-id>.
Step 2: Use QUnit's done Callback to Ping on Success
QUnit exposes a QUnit.done() hook that fires after all tests have run, receiving a summary object with failed, passed, and total counts.
For Node.js (qunit npm package)
Create a setup file qunit-vigilmon.js:
// qunit-vigilmon.js
const https = require("https");
const QUnit = require("qunit");
QUnit.done(function (details) {
const heartbeatUrl = process.env.VIGILMON_HEARTBEAT_URL;
if (!heartbeatUrl) {
return; // skip in local dev if the env var isn't set
}
// Only ping when everything passed
if (details.failed > 0) {
return; // let the heartbeat window expire — Vigilmon will alert
}
if (details.total === 0) {
return; // zero tests ran — do not ping; treat as a failure condition
}
const url = new URL(heartbeatUrl);
const req = https.request(
{
hostname: url.hostname,
path: url.pathname + url.search,
method: "POST",
timeout: 10000,
},
(res) => {
res.resume();
}
);
req.on("error", (err) => {
console.error("[Vigilmon] Heartbeat ping failed:", err.message);
});
req.on("timeout", () => {
req.destroy();
console.error("[Vigilmon] Heartbeat ping timed out");
});
req.end();
});
Load this file alongside your test entry point so the hook is registered before tests run. In your package.json:
{
"scripts": {
"test": "qunit qunit-vigilmon.js tests/**/*.js"
}
}
Or if you use a qunit config file (qunit.config.js):
// qunit.config.js
module.exports = {
files: ["qunit-vigilmon.js", "tests/**/*.js"],
};
Step 3: Browser-Based QUnit Setup
If you run QUnit in a browser (e.g. via Karma or a standalone HTML page), the fetch API replaces the Node.js https module:
// vigilmon-setup.js (browser)
QUnit.done(function (details) {
const heartbeatUrl = window.VIGILMON_HEARTBEAT_URL || "";
if (!heartbeatUrl || details.failed > 0 || details.total === 0) {
return;
}
// Use sendBeacon for fire-and-forget; fall back to fetch
if (navigator.sendBeacon) {
navigator.sendBeacon(heartbeatUrl);
} else {
fetch(heartbeatUrl, { method: "POST", keepalive: true }).catch(() => {});
}
});
Inject VIGILMON_HEARTBEAT_URL into the page from your build tool:
<script>
window.VIGILMON_HEARTBEAT_URL = "https://vigilmon.online/api/heartbeat/<your-id>";
</script>
<script src="vigilmon-setup.js"></script>
Step 4: Integrate with GitHub Actions
# .github/workflows/test.yml
name: QUnit Tests
on:
push:
branches: [main, develop]
schedule:
- cron: "0 3 * * *" # nightly 03:00 UTC
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run QUnit
env:
VIGILMON_HEARTBEAT_URL: ${{ secrets.VIGILMON_QUNIT_HEARTBEAT_URL }}
run: npm test
# QUnit.done fires here on success and pings Vigilmon
Store the heartbeat URL in GitHub repo → Settings → Secrets and variables → Actions → New repository secret:
- Name:
VIGILMON_QUNIT_HEARTBEAT_URL - Value: ping URL from Step 1
If you prefer to handle the ping at the workflow level:
- name: Run QUnit
run: npm test
- name: Ping Vigilmon heartbeat
if: success()
run: |
curl -fsS -X POST "${{ secrets.VIGILMON_QUNIT_HEARTBEAT_URL }}" \
--max-time 10 \
--retry 3 \
--retry-delay 2
Step 5: Integrate with GitLab CI
# .gitlab-ci.yml
test:qunit:
image: node:20-alpine
stage: test
script:
- npm ci
- VIGILMON_HEARTBEAT_URL=$VIGILMON_QUNIT_HEARTBEAT_URL npm test
only:
- main
- schedules
Set VIGILMON_QUNIT_HEARTBEAT_URL in GitLab → Settings → CI/CD → Variables.
For the after-script approach:
test:qunit:
image: node:20-alpine
stage: test
script:
- npm ci
- npm test
after_script:
- |
if [ "$CI_JOB_STATUS" = "success" ]; then
curl -fsS -X POST "$VIGILMON_QUNIT_HEARTBEAT_URL" --max-time 10 --retry 3
fi
Step 6: Monitor Multiple Test Modules Separately
QUnit projects often have distinct modules (core library tests, plugin tests, integration tests) that run on different schedules. Create one heartbeat monitor per module group:
| Module group | Entry file | Monitor name | Interval |
|---|---|---|---|
| Core tests | tests/core.js | QUnit Core — push | 2 h |
| Plugin tests | tests/plugins.js | QUnit Plugins — push | 4 h |
| Integration tests | tests/integration.js | QUnit Integration — nightly | 25 h |
# .github/workflows/test.yml (multi-module)
jobs:
core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20", cache: "npm" }
- run: npm ci
- run: qunit qunit-vigilmon.js tests/core.js
env:
VIGILMON_HEARTBEAT_URL: ${{ secrets.VIGILMON_QUNIT_CORE_HEARTBEAT_URL }}
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20", cache: "npm" }
- run: npm ci
- run: qunit qunit-vigilmon.js tests/integration.js
env:
VIGILMON_HEARTBEAT_URL: ${{ secrets.VIGILMON_QUNIT_INTEGRATION_HEARTBEAT_URL }}
Step 7: Alert Channels
In Vigilmon, go to Notifications → New Channel and configure:
- Email — immediate alert when a heartbeat is missed
- Slack webhook — ping your
#devor#ci-alertschannel
When a QUnit suite stops pinging:
🔴 MISSED HEARTBEAT: QUnit Core — push
Last ping: 3 hours ago
Expected interval: 2 hours
When it recovers:
✅ HEARTBEAT RECOVERED: QUnit Core — push
Gap: 3 hours
What You're Now Catching
| Silent failure mode | How Vigilmon detects it |
|---|---|
| CI path filter excludes test files | No tests run, done never fires, no ping → alert |
| Import error before any tests register | Process exits non-zero, no ping → alert |
| CI job disabled or never triggered | No ping within interval → alert |
| Async test hangs indefinitely | QUnit times out, no done → no ping → alert |
| Zero tests run (total === 0) | Hook skips ping intentionally → alert |
| Tests fail and suite exits non-zero | Hook skips ping → alert after grace period |
QUnit reports every assertion. Vigilmon reports every time QUnit doesn't run. Both signals together mean no failure goes unnoticed.
Start monitoring your QUnit pipelines today — register free at vigilmon.online.