Skip to content

Commit c816b09

Browse files
test: add unit tests
1 parent 3816247 commit c816b09

4 files changed

Lines changed: 456 additions & 1 deletion

File tree

.github/workflows/automated-tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ jobs:
2222
cache: npm
2323
- name: Install dependencies
2424
run: npm ci
25+
- name: Run unit tests
26+
run: node --run test:unit
2527
- name: Check linting
2628
run: node --run lint

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"lint:fix": "eslint --fix && prettier . --write",
1818
"preinstall": "./scripts/preinstall.sh",
1919
"release": "commit-and-tag-version --config ./changelog.config.js",
20-
"test": "node --run lint"
20+
"test": "node --run test:unit && node --run lint",
21+
"test:unit": "node --test tests/**/*.test.mjs"
2122
},
2223
"devDependencies": {
2324
"@eslint/css": "^1.2.0",

tests/moduleLifecycle.test.mjs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import test from "node:test";
5+
import url from "node:url";
6+
import vm from "node:vm";
7+
8+
const {describe, it} = test;
9+
const {fileURLToPath} = url;
10+
11+
const createSandbox = () => {
12+
const sandbox = {
13+
clearInterval,
14+
clearTimeout,
15+
document: {
16+
getElementById: () => null
17+
},
18+
KeyHandler: {
19+
register: () => undefined,
20+
unregister: () => undefined
21+
},
22+
location: {
23+
hostname: "localhost",
24+
port: "8080"
25+
},
26+
Log: {
27+
debug: () => undefined,
28+
error: () => undefined,
29+
log: () => undefined,
30+
warn: () => undefined
31+
},
32+
MM: {
33+
getModules: () => ({
34+
enumerate: () => undefined
35+
})
36+
},
37+
Module: {
38+
register: (_name, definition) => {
39+
sandbox.definition = definition;
40+
}
41+
},
42+
setInterval,
43+
setTimeout
44+
};
45+
46+
sandbox.global = sandbox;
47+
48+
return sandbox;
49+
};
50+
51+
const loadDefinition = () => {
52+
const testsDir = path.dirname(fileURLToPath(import.meta.url));
53+
const modulePath = path.resolve(testsDir, "../MMM-RTSPStream.js");
54+
const code = fs.readFileSync(modulePath, "utf8");
55+
const sandbox = createSandbox();
56+
57+
vm.createContext(sandbox);
58+
vm.runInContext(code, sandbox, {filename: "MMM-RTSPStream.js"});
59+
60+
return {definition: sandbox.definition, sandbox};
61+
};
62+
63+
const createInstance = (definition, options = {}) => {
64+
const streamDefaults = {...definition.defaults.stream1};
65+
const streamConfigOverride = options.config?.stream1 ?? {};
66+
const config = {
67+
...definition.defaults,
68+
...options.config,
69+
stream1: {
70+
...streamDefaults,
71+
...streamConfigOverride
72+
}
73+
};
74+
75+
const instance = {
76+
...definition,
77+
config,
78+
currentStream: "stream1",
79+
name: "MMM-RTSPStream",
80+
playing: false,
81+
streams: options.streams || {
82+
stream1: {
83+
playing: false
84+
}
85+
},
86+
suspended: false
87+
};
88+
89+
return Object.assign(instance, options.props || {});
90+
};
91+
92+
describe("module lifecycle and UI helpers", () => {
93+
it("rotateStream wraps around and updates snapshot state", () => {
94+
const {definition} = loadDefinition();
95+
const notifications = [];
96+
const snapshotCalls = [];
97+
const instance = createInstance(definition, {
98+
streams: {
99+
stream1: {playing: false},
100+
stream2: {playing: false},
101+
stream3: {playing: false}
102+
},
103+
props: {
104+
currentIndex: 2,
105+
currentStream: "stream3",
106+
playing: false,
107+
playSnapshots: (stream) => {
108+
snapshotCalls.push(stream);
109+
},
110+
sendSocketNotification: (type, payload) => {
111+
notifications.push({payload, type});
112+
}
113+
}
114+
});
115+
116+
instance.rotateStream();
117+
118+
assert.equal(instance.currentIndex, 0);
119+
assert.equal(instance.currentStream, "stream1");
120+
assert.equal(snapshotCalls[0], "stream1");
121+
assert.deepEqual(notifications[0], {payload: "stream3", type: "SNAPSHOT_STOP"});
122+
});
123+
124+
it("suspend stops streams and resumed restarts rotation", () => {
125+
const {definition} = loadDefinition();
126+
const calls = [];
127+
const instance = createInstance(definition, {
128+
config: {
129+
autoStart: true,
130+
rotateStreams: true
131+
},
132+
props: {
133+
loaded: true,
134+
selectStream: (_direction, clear) => {
135+
calls.push({clear, fn: "selectStream"});
136+
},
137+
selectedStream: "stream2",
138+
setupStreamRotation: () => {
139+
calls.push({fn: "setupStreamRotation"});
140+
},
141+
stopAllStreams: (startSnapshots) => {
142+
calls.push({fn: "stopAllStreams", startSnapshots});
143+
}
144+
}
145+
});
146+
147+
instance.suspend();
148+
assert.equal(instance.suspended, true);
149+
assert.deepEqual(calls[0], {fn: "stopAllStreams", startSnapshots: false});
150+
assert.deepEqual(calls[1], {clear: true, fn: "selectStream"});
151+
152+
instance.resumed();
153+
assert.equal(instance.suspended, false);
154+
assert.deepEqual(calls[2], {fn: "setupStreamRotation"});
155+
});
156+
157+
it("setWhepStatus toggles overlay visibility and level", () => {
158+
const {definition, sandbox} = loadDefinition();
159+
const rotateOverlayId = "status_";
160+
const streamOverlayId = "status_stream1";
161+
const overlayById = new Map([
162+
[rotateOverlayId, {className: "", textContent: ""}],
163+
[streamOverlayId, {className: "", textContent: ""}]
164+
]);
165+
const requestedIds = [];
166+
sandbox.document.getElementById = (id) => {
167+
requestedIds.push(id);
168+
return overlayById.get(id) || null;
169+
};
170+
171+
const instance = createInstance(definition, {
172+
config: {
173+
rotateStreams: false,
174+
showWhepStatusOverlay: true
175+
}
176+
});
177+
178+
instance.setWhepStatus("stream1", "Reconnecting", "warn");
179+
assert.equal(requestedIds[0], streamOverlayId);
180+
assert.equal(overlayById.get(streamOverlayId).textContent, "Reconnecting");
181+
assert.equal(overlayById.get(streamOverlayId).className, "MMM-RTSPStream statusOverlay warn");
182+
183+
instance.setWhepStatus("stream1", "", "info");
184+
assert.equal(overlayById.get(streamOverlayId).className, "MMM-RTSPStream statusOverlay info hidden");
185+
186+
instance.config.rotateStreams = true;
187+
instance.setWhepStatus("stream1", "Failed", "error");
188+
assert.equal(overlayById.get(rotateOverlayId).textContent, "Failed");
189+
assert.equal(overlayById.get(rotateOverlayId).className, "MMM-RTSPStream statusOverlay error");
190+
assert.equal(requestedIds.at(-1), rotateOverlayId);
191+
});
192+
});

0 commit comments

Comments
 (0)