This document describes the state of the automated test suite for MMM-Remote-Control and the principles that guide it.
- Runner: Node's built-in test runner (
node --test) – lean, zero-config, future-proof. - Coverage:
c8with enforced thresholds (30% statements/lines, 20% functions, 60% branches). - Quality gates: Lint (
node --run lint) and spell check (node --run test:spelling) are part of the standardnode --run testpipeline. - Execution shortcuts:
- All tests:
node --run test(includes unit + integration + DOM) - Unit tests only:
node --run test:unit - HTTP-layer tests only:
node --run test:integration - DOM tests only:
node --run test:dom - Coverage report:
node --run test:coverage - Watch mode:
node --run test:watch
- All tests:
tests/
├── unit/ # Isolated logic tests with mocked dependencies
├── integration/ # HTTP-layer tests with real Express routing
├── dom/ # DOM logic tests using happy-dom (no browser required)
└── shims/ # Minimal stubs for MagicMirror globals
Unit tests verify individual functions in isolation. Fast, deterministic, no I/O.
Integration tests start a real Express server and make HTTP requests. They catch:
- Route wiring bugs (wrong paths, missing endpoints)
- Middleware ordering issues
- JSON parsing/serialization problems
- Authentication/error response formats
DOM tests verify frontend logic (remote.js) in a simulated DOM environment using happy-dom. Fast, CI-friendly, no browser required.
All tests run in CI/CD without MagicMirror runtime or browser dependencies.
| Suite | Purpose |
|---|---|
answerPost.config.test.js |
Config persistence: backup rotation, write-error propagation, UNDO_CONFIG restore flow |
answerGet.contract.test.js |
Response shapes, data assembly logic, and schema validation for module/config endpoints |
answerGet.saves.test.js |
Backup timestamp ordering and ENOENT handling for /api/saves |
api.answerModuleApi.test.js |
Module API edge cases and action routing |
api.mergeData.test.js |
Config merging with external API routes |
api.delayedFlow.test.js, delayedQuery.test.js |
/delay wrapper, timer scheduling, reset, abort semantics |
executeQuery.core.test.js |
Module visibility, notifications, system actions (SHOW/HIDE/REFRESH/RESTART) |
executeQuery.error.test.js |
Error handling for malformed JSON, missing params |
api.helpers.test.js |
JSON payload parsing, delay parameter handling |
executeQuery.alerts.test.js |
Alert handling (SHOW_ALERT/HIDE_ALERT parameter handling) |
executeQuery.notifications.test.js |
Notification wrapper functions and routing logic |
executeQuery.handlers.test.js |
Action handlers, delayed queries, callback scheduling |
helper.config.test.js |
Config path resolution, module directory handling |
helper.utils.test.js |
Response handling, translation, error checking, network utilities |
answerGetChangelog.test.js |
Changelog file retrieval and error handling |
socketNotification.test.js |
Socket notification routing (CURRENT_STATUS, REQUEST_DEFAULT_SETTINGS, REMOTE_ACTION) |
helper.getConfig.test.js |
Config merging with defaults from moduleData |
utils.test.js, configUtils.test.js |
String-format helpers and cleanConfig regressions |
| Suite | Purpose |
|---|---|
remote.smoke.test.js |
Frontend logic tests using happy-dom (no browser required) |
MMM-Remote-Control.test.js |
Module initialization and lockStrings handling with edge case checks |
| Suite | Purpose |
|---|---|
api.smoke.test.js |
HTTP-layer smoke tests for Express routing and API contracts |
Together these suites focus on isolated logic (unit tests) and HTTP contract verification (HTTP-layer tests)—the most critical areas for catching regressions while remaining CI-friendly.
As of December 2024, actual coverage stands at:
| Metric | Value | Threshold | Notes |
|---|---|---|---|
| Statements | ~49% | 30% | Reached plateau with core logic covered |
| Branches | ~83% | 60% | Strong branch coverage |
| Functions | ~57% | 20% | Major handlers and utilities tested |
| Lines | ~49% | 30% | Mirrors statement coverage |
node_helper.js |
~28% | - | Core actions and notification flow covered |
API/api.js |
~62% | - | Edge cases and routing verified |
Thresholds now provide meaningful regression protection while remaining achievable. Further gains would require testing system commands and hardware integration better suited for E2E or manual testing.
- Router-to-handler wiring: Express route-mapping tests duplicated framework behavior and broke on route reordering. Manual smoke tests or integration tests are better.
- System commands and hardware control (
shutdown,reboot, monitor control): Depend on Raspberry Pi hardware privileges and side effects we can't safely stub in CI. - E2E browser tests: Full Puppeteer/Playwright tests with real browsers are heavy and flaky. We test frontend logic with happy-dom instead (fast, deterministic, CI-friendly).
- "Pass-through" notification wrappers: Flows that simply forward parameters (
HIDE_ALERT,SHOW_ALERT, etc.) are exercised indirectly; duplicating them would be noise. - Git/network dependent paths: Update and install code paths require repositories and network access. We guard their public contract via
getExternalApiByGuessing/menu tests instead.
Documenting these gaps helps us recognize when a change might require a different kind of test (manual check, integration smoke, etc.).
- Minimal shims live under
tests/shims/(forloggerandnode_helper). Tests extend the shim viaNODE_PATHbefore importing the module under test. - Unit tests: Stub
fsmethods directly, ensuring no real disk I/O. Use helper factories to clone module exports and override context methods (e.g.,sendResponse,callAfterUpdate). - HTTP-layer tests: Mock MagicMirror globals (
Module,Log) but use real Express routing and HTTP layer. Capture socket notifications instead of sending them. Bind real API/helper methods to mock context. Focus is on API contracts and Express wiring, not end-to-end MagicMirror integration.
The HTTP-layer smoke test (tests/integration/api.smoke.test.js) tests Express routing and API contracts without requiring full MagicMirror runtime:
What's real:
- Express app and router
- HTTP requests/responses (using
fetch) - Middleware execution (JSON parsing, authentication)
- Route matching and parameter extraction
What's mocked:
- MagicMirror globals (
Module,Log) - Socket notifications (captured in array, not sent)
- File system operations
- Module-specific logic requiring runtime
What this tests (and doesn't):
✅ Does catch:
- Route wiring bugs (wrong paths, missing routes)
- Middleware issues (wrong order, missing JSON parser)
- Response format changes (breaking API contracts)
- HTTP-level errors (400/403/500 responses)
- Content-Type validation
❌ Does NOT catch:
- MagicMirror module interaction bugs
- Socket notification delivery to frontend
- Config file parsing from disk
- Module lifecycle (show/hide actual modules)
- External module API discovery
Trade-off rationale:
These are HTTP-layer tests, not end-to-end integration tests. They verify Express routing and API contracts without MagicMirror runtime. They sit between pure unit tests (which don't test HTTP at all) and full E2E tests (which require Electron/browser and can't run in CI). They're valuable for catching route wiring bugs, middleware issues, and API contract breakage, but don't replace manual testing with actual MagicMirror.
Key learnings:
- Shims are essential: Reuse
tests/shims/setup to resolve MagicMirror dependencies - Method binding matters: API methods call
this.answerGet()etc. – bind them to mock context with.bind(mockContext) - Minimal stubs suffice: Only stub methods actually called during test execution (
getExternalApiByGuessing,updateModuleApiMenu) - Random ports work: Use
server.listen(0)to get a free port, avoiding conflicts - CI-compatible: No browser/Electron/hardware dependencies – runs in any Node.js environment
This approach catches route wiring bugs, middleware issues, and response format problems that unit tests miss, while remaining fast and deterministic.
- Full E2E tests with Puppeteer/Playwright
- Hardware-dependent command testing (shutdown, reboot, monitor control)
- Git/network-dependent install/update flows
As of December 2025, coverage has reached a healthy plateau through incremental, high-value testing:
- ~49% statements – Core logic paths covered
- ~57% functions – Major handlers tested
- ~83% branches – Strong conditional coverage
- 152 tests – Mix of unit, integration, and DOM tests
Further coverage gains would hit diminishing returns:
- Remaining untested code is primarily system commands (
shutdown,reboot, monitor control) requiring hardware/privileges - Complex integration points (PM2, Electron, git operations) need E2E tests, not unit tests
- Express routing details already verified at HTTP layer
- Mock complexity would exceed test value
- Test behavior, not coverage – Every test should catch real bugs or prevent regressions
- Minimal mocking – If a test needs >5 mocks, reconsider the abstraction
- Skip brittle integration points – File system, network, hardware, DOM manipulation better suited for E2E
- Avoid duplication – Don't re-test what HTTP-layer tests already cover
- Edge cases only – For utilities (
lib/configUtils.js,lib/utils.js), add tests when behavior changes
- Prefer deterministic unit tests with explicit stubbing over fragile integration harnesses.
- Question every prospective test: if it simply mirrors production code without asserting behavior, it's likely not worth adding.
- Keep pull requests focused—group related assertions in the same suite and avoid cross-cutting rewrites.
- Restore any global/mocked state (
Module._load, timers,fs) inafterEachblocks to keep suites isolated. - Use
describe/testfromnode:testdirectly – no aliases or compatibility shims.
Maintaining this lean, purpose-built suite gives fast feedback on the project's riskiest logic without overwhelming contributors with maintenance burden.