Skip to content

Commit 9407417

Browse files
test(structure): group config/utils/delayedQuery tests and clarify titles
1 parent fc55870 commit 9407417

File tree

9 files changed

+272
-256
lines changed

9 files changed

+272
-256
lines changed

tests/README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Status checkpoint (2025-09-21):
2323

2424
- Phases 1 and 2 completed. We added unit tests for API helpers (`checkDelay`, `answerNotifyApi`), `answerModuleApi` defaults and SHOW flow, and timer behavior for `delayedQuery`.
2525
- Introduced shims for `logger` and `node_helper` so `node_helper.js` can be required in tests without MagicMirror runtime.
26-
- One `answerGet` test is currently skipped due to tight coupling to MM core; plan is to re-cover at the API router level or further isolate dependencies.
26+
- 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.
2727

2828
## Test Roadmap (Phases)
2929

@@ -33,12 +33,11 @@ The project follows an incremental test roadmap. Coverage thresholds start low a
3333
- Lint, formatter, spellcheck wired; no runtime tests.
3434
2. Phase 2 — Test runner & utilities (Done)
3535
- Test runner configured, first unit tests added, coverage baseline established.
36-
3. Phase 3 — More unit & first integration tests (In progress)
36+
3. Phase 3 — More unit & first integration tests (Done)
3737
- Extra edge cases for `cleanConfig` (Done)
38-
- GET routes: `/get?data=moduleAvailable`, `/get?data=config`, error path (Pending)
39-
- Note: One `answerGet` unit test is currently skipped; target to re-cover via API router tests or further isolation.
40-
- Small express/test factory (mocks: fs, simple-git) (Pending)
41-
- Raise coverage target (e.g., statements >8%) (Pending)
38+
- GET routes covered at router level (Done)
39+
- Small express/test factory (mocks: fs, simple-git) (Deferred)
40+
- Raise coverage target (Done — thresholds bumped slightly)
4241
4. Phase 4 — Action / socket logic (core `executeQuery` paths) (In progress)
4342
- DELAYED timer (start, reset, abort) (Done)
4443
- BRIGHTNESS, TEMP, NOTIFICATION parsing, HIDE/SHOW/TOGGLE selection logic (Pending)

tests/unit/answerGet.test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
const {test} = require("node:test");
22

3-
// Skipped: Requires MagicMirror core defaults and NodeHelper; keep as placeholder
3+
/*
4+
* TODO(later phase): Direct unit coverage of answerGet when we further isolate NodeHelper/MM core.
5+
* Router-level GET coverage is in tests/unit/api.getRoutes.mapping.test.js (Phase 3 done).
6+
*/
47
test.skip("answerGet GET paths: moduleAvailable/config/error", () => {});
Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const assert = require("node:assert/strict");
2-
const {test} = require("node:test");
2+
const {test, describe} = require("node:test");
3+
const group = typeof describe === "function" ? describe : (_n, fn) => fn();
34

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

@@ -21,40 +22,42 @@ function makeCtx (overrides = {}) {
2122
};
2223
}
2324

24-
test("answerModuleApi DEFAULTS calls answerGet with defaultConfig", () => {
25-
const captured = {};
26-
const ctx = makeCtx({
27-
mergeData: () => ({success: true, data: [{identifier: "module_1_weather", name: "weather", urlPath: "weather", actions: {}}]}),
28-
answerGet: (query, res) => {
29-
captured.query = query;
30-
if (res && res.json) res.json({ok: true});
31-
}
25+
group("Module API", () => {
26+
test("DEFAULTS action requests defaultConfig via answerGet", () => {
27+
const captured = {};
28+
const ctx = makeCtx({
29+
mergeData: () => ({success: true, data: [{identifier: "module_1_weather", name: "weather", urlPath: "weather", actions: {}}]}),
30+
answerGet: (query, res) => {
31+
captured.query = query;
32+
if (res && res.json) res.json({ok: true});
33+
}
34+
});
35+
const answerModuleApi = apiModule.answerModuleApi.bind(ctx);
36+
37+
const req = {params: {moduleName: "weather", action: "defaults"}};
38+
const res = {status: () => ({json: () => {}}), json: () => {}};
39+
answerModuleApi(req, res);
40+
41+
assert.deepEqual(captured.query, {data: "defaultConfig", module: "weather"});
3242
});
33-
const answerModuleApi = apiModule.answerModuleApi.bind(ctx);
3443

35-
const req = {params: {moduleName: "weather", action: "defaults"}};
36-
const res = {status: () => ({json: () => {}}), json: () => {}};
37-
answerModuleApi(req, res);
38-
39-
assert.deepEqual(captured.query, {data: "defaultConfig", module: "weather"});
40-
});
41-
42-
test("answerModuleApi SHOW all triggers executeQuery with module all", () => {
43-
const captured = {};
44-
const ctx = makeCtx({
45-
// Minimal module data so filtering passes when moduleName === 'all'
46-
configData: {moduleData: [{identifier: "module_1_test", name: "test", urlPath: "test"}]},
47-
executeQuery: (query) => { captured.query = query; },
48-
sendSocketNotification: () => {},
49-
// No delay in this test
50-
checkDelay: (q) => q
44+
test("SHOW action on 'all' triggers executeQuery with module all", () => {
45+
const captured = {};
46+
const ctx = makeCtx({
47+
// Minimal module data so filtering passes when moduleName === 'all'
48+
configData: {moduleData: [{identifier: "module_1_test", name: "test", urlPath: "test"}]},
49+
executeQuery: (query) => { captured.query = query; },
50+
sendSocketNotification: () => {},
51+
// No delay in this test
52+
checkDelay: (q) => q
53+
});
54+
const answerModuleApi = apiModule.answerModuleApi.bind(ctx);
55+
56+
const req = {params: {moduleName: "all", action: "show"}};
57+
const res = {json: () => {}};
58+
answerModuleApi(req, res);
59+
60+
assert.equal(captured.query.action, "SHOW");
61+
assert.equal(captured.query.module, "all");
5162
});
52-
const answerModuleApi = apiModule.answerModuleApi.bind(ctx);
53-
54-
const req = {params: {moduleName: "all", action: "show"}};
55-
const res = {json: () => {}};
56-
answerModuleApi(req, res);
57-
58-
assert.equal(captured.query.action, "SHOW");
59-
assert.equal(captured.query.module, "all");
6063
});

tests/unit/api.delayedFlow.test.js

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const assert = require("node:assert/strict");
2-
const {test} = require("node:test");
2+
const {test, describe} = require("node:test");
3+
const group = typeof describe === "function" ? describe : (_n, fn) => fn();
34

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

@@ -21,25 +22,27 @@ function makeCtx (overrides = {}) {
2122
};
2223
}
2324

24-
test("answerNotifyApi wraps into DELAYED when /delay used", () => {
25-
const captured = {};
26-
const ctx = makeCtx({
27-
delayedQuery: (query) => { captured.query = query; }
28-
});
29-
const answerNotifyApi = apiModule.answerNotifyApi.bind(ctx);
25+
group("Delayed flow (/delay)", () => {
26+
test("answerNotifyApi wraps query into DELAYED and preserves payload", () => {
27+
const captured = {};
28+
const ctx = makeCtx({
29+
delayedQuery: (query) => { captured.query = query; }
30+
});
31+
const answerNotifyApi = apiModule.answerNotifyApi.bind(ctx);
3032

31-
const req = {
32-
method: "GET",
33-
params: {notification: "HELLO", delayed: "delay"},
34-
query: {did: "ID1", timeout: 5}
35-
};
36-
const res = {json: () => {}};
33+
const req = {
34+
method: "GET",
35+
params: {notification: "HELLO", delayed: "delay"},
36+
query: {did: "ID1", timeout: 5}
37+
};
38+
const res = {json: () => {}};
3739

38-
answerNotifyApi(req, res);
40+
answerNotifyApi(req, res);
3941

40-
assert.equal(captured.query.action, "DELAYED");
41-
assert.equal(captured.query.did, "ID1");
42-
assert.equal(captured.query.timeout, 5);
43-
assert.equal(captured.query.query.action, "NOTIFICATION");
44-
assert.equal(captured.query.query.notification, "HELLO");
42+
assert.equal(captured.query.action, "DELAYED");
43+
assert.equal(captured.query.did, "ID1");
44+
assert.equal(captured.query.timeout, 5);
45+
assert.equal(captured.query.query.action, "NOTIFICATION");
46+
assert.equal(captured.query.query.notification, "HELLO");
47+
});
4548
});

tests/unit/api.helpers.test.js

Lines changed: 56 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const assert = require("node:assert/strict");
2-
const {test} = require("node:test");
2+
const {test, describe} = require("node:test");
3+
const group = typeof describe === "function" ? describe : (_n, fn) => fn();
34

45
// We'll import the module and bind methods to a fake context so we can test pure-ish helpers
56
const apiModule = require("../../API/api.js");
@@ -20,71 +21,66 @@ function makeCtx (overrides = {}) {
2021
};
2122
}
2223

23-
test("checkDelay adds DELAYED wrapper and UUID if '/delay' is used", () => {
24-
const ctx = makeCtx();
25-
const checkDelay = apiModule.checkDelay.bind(ctx);
26-
const req = {params: {delayed: "delay"}, query: {}, body: {}};
27-
const q = {action: "RESTART"};
28-
const out = checkDelay(q, req);
29-
assert.equal(out.action, "DELAYED");
30-
assert.ok(out.did && typeof out.did === "string");
31-
assert.equal(out.timeout, 10);
32-
assert.deepEqual(out.query, q);
33-
});
34-
35-
test("checkDelay keeps original query when no '/delay'", () => {
36-
const ctx = makeCtx();
37-
const checkDelay = apiModule.checkDelay.bind(ctx);
38-
const req = {params: {}, query: {}, body: {}};
39-
const q = {action: "REFRESH"};
40-
const out = checkDelay(q, req);
41-
assert.equal(out, q);
42-
});
24+
group("API helpers", () => {
25+
group("checkDelay", () => {
26+
test("wraps query as DELAYED when '/delay' is used (adds ID and default timeout)", () => {
27+
const ctx = makeCtx();
28+
const checkDelay = apiModule.checkDelay.bind(ctx);
29+
const req = {params: {delayed: "delay"}, query: {}, body: {}};
30+
const q = {action: "RESTART"};
31+
const out = checkDelay(q, req);
32+
assert.equal(out.action, "DELAYED");
33+
assert.ok(out.did && typeof out.did === "string");
34+
assert.equal(out.timeout, 10);
35+
assert.deepEqual(out.query, q);
36+
});
4337

44-
test("answerNotifyApi builds payload from query and params (GET)", () => {
45-
const captured = {};
46-
const ctx = makeCtx({
47-
sendSocketNotification: (what, payload) => {
48-
captured.what = what;
49-
captured.payload = payload;
50-
}
38+
test("keeps original query when '/delay' is not used", () => {
39+
const ctx = makeCtx();
40+
const checkDelay = apiModule.checkDelay.bind(ctx);
41+
const req = {params: {}, query: {}, body: {}};
42+
const q = {action: "REFRESH"};
43+
const out = checkDelay(q, req);
44+
assert.equal(out, q);
45+
});
5146
});
52-
const answerNotifyApi = apiModule.answerNotifyApi.bind(ctx);
5347

54-
const req = {
55-
method: "GET",
56-
params: {notification: "TEST_ACTION"},
57-
query: {foo: "bar"}
58-
};
59-
const res = {json: (obj) => { captured.response = obj; }};
60-
answerNotifyApi(req, res);
48+
group("answerNotifyApi", () => {
49+
test("builds payload from GET params and returns success", () => {
50+
const captured = {};
51+
const ctx = makeCtx({
52+
sendSocketNotification: (what, payload) => {
53+
captured.what = what;
54+
captured.payload = payload;
55+
}
56+
});
57+
const answerNotifyApi = apiModule.answerNotifyApi.bind(ctx);
6158

62-
assert.equal(captured.what, "NOTIFICATION");
63-
assert.equal(captured.payload.notification, "TEST_ACTION");
64-
assert.deepEqual(captured.response, {success: true, notification: "TEST_ACTION", payload: {foo: "bar"}});
65-
});
59+
const req = {method: "GET", params: {notification: "TEST_ACTION"}, query: {foo: "bar"}};
60+
const res = {json: (obj) => { captured.response = obj; }};
61+
answerNotifyApi(req, res);
6662

67-
test("answerNotifyApi merges POST body into payload and supports action payload", () => {
68-
const captured = {};
69-
const ctx = makeCtx({
70-
sendSocketNotification: (what, payload) => { captured.payload = payload; }
71-
});
72-
const answerNotifyApi = apiModule.answerNotifyApi.bind(ctx);
63+
assert.equal(captured.what, "NOTIFICATION");
64+
assert.equal(captured.payload.notification, "TEST_ACTION");
65+
assert.deepEqual(captured.response, {success: true, notification: "TEST_ACTION", payload: {foo: "bar"}});
66+
});
7367

74-
const req = {
75-
method: "POST",
76-
params: {notification: "TEST_ACTION"},
77-
query: {alpha: 1},
78-
body: {beta: 2}
79-
};
80-
const res = {json: () => {}};
68+
test("merges POST body and action payload into final payload", () => {
69+
const captured = {};
70+
const ctx = makeCtx({sendSocketNotification: (what, payload) => { captured.payload = payload; }});
71+
const answerNotifyApi = apiModule.answerNotifyApi.bind(ctx);
8172

82-
const action = {notification: "TEST_ACTION", payload: {gamma: 3}};
83-
answerNotifyApi(req, res, action);
73+
const req = {method: "POST", params: {notification: "TEST_ACTION"}, query: {alpha: 1}, body: {beta: 2}};
74+
const res = {json: () => {}};
75+
const action = {notification: "TEST_ACTION", payload: {gamma: 3}};
8476

85-
// payload should contain alpha, beta, and action payload gamma
86-
assert.equal(captured.payload.notification, "TEST_ACTION");
87-
assert.equal(captured.payload.payload.alpha, 1);
88-
assert.equal(captured.payload.payload.beta, 2);
89-
assert.equal(captured.payload.payload.gamma, 3);
77+
answerNotifyApi(req, res, action);
78+
79+
// payload should contain alpha, beta, and action payload gamma
80+
assert.equal(captured.payload.notification, "TEST_ACTION");
81+
assert.equal(captured.payload.payload.alpha, 1);
82+
assert.equal(captured.payload.payload.beta, 2);
83+
assert.equal(captured.payload.payload.gamma, 3);
84+
});
85+
});
9086
});
Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
11
const assert = require("node:assert/strict");
2-
const {test} = require("node:test");
2+
const {test, describe} = require("node:test");
33
const {cleanConfig} = require("../../lib/configUtils");
4+
const group = typeof describe === "function" ? describe : (_n, fn) => fn();
45

5-
test("cleanConfig leaves non-default top-level keys and removes defaults deeply (arrays/objects)", () => {
6-
const defaultConfig = {language: "en", header: {enabled: true}, list: [1, 2, 3]};
7-
const moduleDefaultsMap = {foo: {arr: [1, 2], obj: {a: 1}}};
8-
const cfg = {
9-
language: "en", // should be removed
10-
header: {enabled: true}, // should be removed (deep equal)
11-
list: [1, 2, 3, 4], // differs -> keep
12-
modules: [{module: "foo", header: "", config: {arr: [1, 2], obj: {a: 1}, extra: 9, position: ""}}]
13-
};
14-
cleanConfig({config: cfg, defaultConfig, moduleDefaultsMap});
15-
assert.ok(!("language" in cfg));
16-
assert.ok(!("header" in cfg));
17-
assert.deepEqual(cfg.list, [1, 2, 3, 4]);
18-
const m = cfg.modules[0];
19-
assert.ok(!("arr" in m.config));
20-
assert.ok(!("obj" in m.config));
21-
assert.equal(m.config.extra, 9);
22-
assert.ok(!("position" in m.config));
23-
assert.ok(!("header" in m));
24-
});
6+
group("configUtils.cleanConfig edge cases", () => {
7+
test("removes deep-equal defaults; preserves differing arrays/objects", () => {
8+
const defaultConfig = {language: "en", header: {enabled: true}, list: [1, 2, 3]};
9+
const moduleDefaultsMap = {foo: {arr: [1, 2], obj: {a: 1}}};
10+
const cfg = {
11+
language: "en", // should be removed
12+
header: {enabled: true}, // should be removed (deep equal)
13+
list: [1, 2, 3, 4], // differs -> keep
14+
modules: [{module: "foo", header: "", config: {arr: [1, 2], obj: {a: 1}, extra: 9, position: ""}}]
15+
};
16+
cleanConfig({config: cfg, defaultConfig, moduleDefaultsMap});
17+
assert.ok(!("language" in cfg));
18+
assert.ok(!("header" in cfg));
19+
assert.deepEqual(cfg.list, [1, 2, 3, 4]);
20+
const m = cfg.modules[0];
21+
assert.ok(!("arr" in m.config));
22+
assert.ok(!("obj" in m.config));
23+
assert.equal(m.config.extra, 9);
24+
assert.ok(!("position" in m.config));
25+
assert.ok(!("header" in m));
26+
});
2527

26-
test("cleanConfig tolerates nulls and unknown modules", () => {
27-
const cfg = {language: null, modules: [{module: "unknown", config: {x: 1}}]};
28-
cleanConfig({config: cfg, defaultConfig: {language: null}, moduleDefaultsMap: {}});
29-
assert.ok(!("language" in cfg));
30-
assert.equal(cfg.modules[0].config.x, 1);
28+
test("tolerates nulls at top level and unknown modules", () => {
29+
const cfg = {language: null, modules: [{module: "unknown", config: {x: 1}}]};
30+
cleanConfig({config: cfg, defaultConfig: {language: null}, moduleDefaultsMap: {}});
31+
assert.ok(!("language" in cfg));
32+
assert.equal(cfg.modules[0].config.x, 1);
33+
});
3134
});

0 commit comments

Comments
 (0)