Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 2 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,65 +301,13 @@ For contributing to this repository, please see the [CONTRIBUTING.md](CONTRIBUTI

### Tests & Quality

Current automated test stack (now at Phase 2) is in place:

Phase 1: basic quality gates
See `tests/README.md` for test setup, commands, and the roadmap. Quick commands:

- Lint & formatting: `node --run lint`
- Spell checking: `node --run test:spelling`

Phase 2: initial unit test & coverage foundation

- Unit tests (Node built-in runner): `node --run test:unit`
- Combined pipeline (lint + spelling + unit): `npm test`
- Unit tests (Node built-in): `node --run test:unit`
- Coverage (c8): `node --run test:coverage`

Coverage thresholds start deliberately low to allow incremental improvements. New tests should raise real coverage and can then justify increasing thresholds in `package.json`.

#### Planned Test Roadmap (Phases)

1. Phase (done): Base quality gates (lint, formatter, spellcheck) – no runtime tests
2. Phase (done): Test runner, extracted utilities, first unit tests, coverage baseline
3. Phase: More unit & first integration tests

- Extra edge cases for `cleanConfig`
- GET routes: `/get?data=moduleAvailable`, `/get?data=config`, error path
- Small express/test factory (mocks: fs, simple-git)
- Raise coverage target (e.g. statements >8%)

4. Phase: Action / socket logic (core `executeQuery` paths)

- BRIGHTNESS, TEMP, NOTIFICATION parsing, HIDE/SHOW/TOGGLE selection logic
- DELAYED timer (start, reset, abort)

5. Phase: Persistence & backups

- `answerPost` (saving config), backup rotation, UNDO_CONFIG, failure scenarios (fs errors, disk edge cases)

6. Phase: Module install & update flows

- `installModule`, `updateModule` with mocked `simple-git` & `exec`

7. Phase: System / hardware related commands

- Monitor control (status detection), shutdown/reboot, PM2 control (pm2 mock)

8. Phase: Frontend / DOM logic (jsdom)

- `getDom()` URL/port logic, brightness filter application, temp overlay color gradients

9. Phase: API / contract tests

- Validate against `docs/swagger.json` (add missing if needed)

10. Phase: Optional E2E / Docker integration

- Spin minimal MagicMirror instance; smoke test key endpoints

11. Phase (ongoing): Gradually raise coverage thresholds & consider mutation tests

Each phase incrementally increases coverage thresholds to encourage steady progress without big-bang changes.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
],
"include": [
"lib/**/*.js",
"API/**/*.js",
"node_helper.js",
"MMM-Remote-Control.js"
],
Expand All @@ -85,9 +86,9 @@
"**/*.md"
],
"check-coverage": true,
"branches": 5,
"lines": 4,
"functions": 4,
"statements": 4
"branches": 6,
"lines": 6,
"functions": 5,
"statements": 6
}
}
57 changes: 57 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Tests

This project uses Node's built-in test runner and c8 for coverage.

Quick run:

- Lint & format check: `node --run lint`
- Unit tests: `node --run test:unit`
- Coverage: `node --run test:coverage`

Guidelines:

- Keep tests side-effect free; mock/bypass filesystem, network, and timers.
- For API helpers in `API/api.js`, bind functions to a fake context rather than spinning an Express server.
- For `node_helper.js` paths, test logic by stubbing methods (e.g., `sendSocketNotification`, `sendResponse`) and, when necessary, mock timers.
- Follow repository lint rules; keep arrays/objects on one line where stylistic rules require it.

Notes on shims used in tests:

- We provide lightweight shims under `tests/shims/` (e.g., `logger.js`, `node_helper.js`) to isolate unit tests from MagicMirror core. When a test needs to load `node_helper.js`, inject the shim path via `NODE_PATH` and call `module._initPaths()` before requiring the target file.

Status checkpoint (2025-09-21):

- Phases 1 and 2 completed. We added unit tests for API helpers (`checkDelay`, `answerNotifyApi`), `answerModuleApi` defaults and SHOW flow, and timer behavior for `delayedQuery`.
- Introduced shims for `logger` and `node_helper` so `node_helper.js` can be required in tests without MagicMirror runtime.
- Phase 3 router-level GET coverage added; one direct `answerGet` unit test remains skipped and is deferred to a later phase once NodeHelper/MM core is further isolated.

## Test Roadmap (Phases)

The project follows an incremental test roadmap. Coverage thresholds start low and rise as we add tests.

1. Phase 1 — Base quality gates (Done)
- Lint, formatter, spellcheck wired; no runtime tests.
2. Phase 2 — Test runner & utilities (Done)
- Test runner configured, first unit tests added, coverage baseline established.
3. Phase 3 — More unit & first integration tests (Done)
- Extra edge cases for `cleanConfig` (Done)
- GET routes covered at router level (Done)
- Small express/test factory (mocks: fs, simple-git) (Deferred)
- Raise coverage target (Done — thresholds bumped slightly)
4. Phase 4 — Action / socket logic (core `executeQuery` paths) (In progress)
- DELAYED timer (start, reset, abort) (Done)
- BRIGHTNESS, TEMP, NOTIFICATION parsing, HIDE/SHOW/TOGGLE selection logic (Pending)
5. Phase 5 — Persistence & backups (Not started)
- `answerPost` (saving config), backup rotation, UNDO_CONFIG, failure scenarios (fs errors, disk edge cases)
6. Phase 6 — Module install & update flows (Not started)
- `installModule`, `updateModule` with mocked `simple-git` & `exec`
7. Phase 7 — System / hardware related commands (Not started)
- Monitor control (status detection), shutdown/reboot, PM2 control (pm2 mock)
8. Phase 8 — Frontend / DOM logic (jsdom) (Not started)
- `getDom()` URL/port logic, brightness filter application, temp overlay color gradients
9. Phase 9 — API / contract tests (Not started)
- Validate against `docs/swagger.json` (add missing if needed)
10. Phase 10 — Optional E2E / Docker integration (Not started)
- Spin minimal MagicMirror instance; smoke test key endpoints
11. Phase 11 — Raise thresholds & mutation tests (Ongoing)
- Gradually raise coverage thresholds; consider mutation testing after core paths stable.
7 changes: 7 additions & 0 deletions tests/shims/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
log: (...args) => console.log(...args),
info: (...args) => console.info(...args),
warn: (...args) => console.warn(...args),
error: (...args) => console.error(...args),
debug: (...args) => console.debug(...args)
};
3 changes: 3 additions & 0 deletions tests/shims/node_helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
create: (obj) => obj
};
7 changes: 7 additions & 0 deletions tests/unit/answerGet.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const {test} = require("node:test");

/*
* TODO(later phase): Direct unit coverage of answerGet when we further isolate NodeHelper/MM core.
* Router-level GET coverage is in tests/unit/api.getRoutes.mapping.test.js (Phase 3 done).
*/
test.skip("answerGet GET paths: moduleAvailable/config/error", () => {});
63 changes: 63 additions & 0 deletions tests/unit/api.answerModuleApi.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const assert = require("node:assert/strict");
const {test, describe} = require("node:test");
const group = typeof describe === "function" ? describe : (_n, fn) => fn();

const apiModule = require("../../API/api.js");

function makeCtx (overrides = {}) {
return {
configOnHd: {modules: [{module: "MMM-Remote-Control", config: {}}]},
externalApiRoutes: {},
moduleApiMenu: {},
translation: {},
configData: {moduleData: []},
mergeData: function () { return {success: true, data: this.configData.moduleData}; },
sendSocketNotification: () => {},
sendResponse: () => {},
checkInitialized: () => true,
translate: (s) => s,
formatName: (s) => s,
thisConfig: {},
...overrides
};
}

group("Module API", () => {
test("DEFAULTS action requests defaultConfig via answerGet", () => {
const captured = {};
const ctx = makeCtx({
mergeData: () => ({success: true, data: [{identifier: "module_1_weather", name: "weather", urlPath: "weather", actions: {}}]}),
answerGet: (query, res) => {
captured.query = query;
if (res && res.json) res.json({ok: true});
}
});
const answerModuleApi = apiModule.answerModuleApi.bind(ctx);

const req = {params: {moduleName: "weather", action: "defaults"}};
const res = {status: () => ({json: () => {}}), json: () => {}};
answerModuleApi(req, res);

assert.deepEqual(captured.query, {data: "defaultConfig", module: "weather"});
});

test("SHOW action on 'all' triggers executeQuery with module all", () => {
const captured = {};
const ctx = makeCtx({
// Minimal module data so filtering passes when moduleName === 'all'
configData: {moduleData: [{identifier: "module_1_test", name: "test", urlPath: "test"}]},
executeQuery: (query) => { captured.query = query; },
sendSocketNotification: () => {},
// No delay in this test
checkDelay: (q) => q
});
const answerModuleApi = apiModule.answerModuleApi.bind(ctx);

const req = {params: {moduleName: "all", action: "show"}};
const res = {json: () => {}};
answerModuleApi(req, res);

assert.equal(captured.query.action, "SHOW");
assert.equal(captured.query.module, "all");
});
});
48 changes: 48 additions & 0 deletions tests/unit/api.delayedFlow.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const assert = require("node:assert/strict");
const {test, describe} = require("node:test");
const group = typeof describe === "function" ? describe : (_n, fn) => fn();

const apiModule = require("../../API/api.js");

function makeCtx (overrides = {}) {
return {
configOnHd: {modules: []},
externalApiRoutes: {},
moduleApiMenu: {},
translation: {},
sendSocketNotification: () => {},
sendResponse: () => {},
checkInitialized: () => true,
checkDelay: apiModule.checkDelay,
translate: (s) => s,
formatName: (s) => s,
delayedQuery: () => {},
thisConfig: {},
...overrides
};
}

group("Delayed flow (/delay)", () => {
test("answerNotifyApi wraps query into DELAYED and preserves payload", () => {
const captured = {};
const ctx = makeCtx({
delayedQuery: (query) => { captured.query = query; }
});
const answerNotifyApi = apiModule.answerNotifyApi.bind(ctx);

const req = {
method: "GET",
params: {notification: "HELLO", delayed: "delay"},
query: {did: "ID1", timeout: 5}
};
const res = {json: () => {}};

answerNotifyApi(req, res);

assert.equal(captured.query.action, "DELAYED");
assert.equal(captured.query.did, "ID1");
assert.equal(captured.query.timeout, 5);
assert.equal(captured.query.query.action, "NOTIFICATION");
assert.equal(captured.query.query.notification, "HELLO");
});
});
Loading