Skip to content

Commit 591e708

Browse files
feat: add unit test setup with node:test
Add comprehensive unit test infrastructure using Node.js native test runner: - 38 tests across 5 utility functions - Tests for mph2Beaufort, roundValue, cardinalWindDirection, convertWeatherType, getOrdinal - Updated CI workflow to run tests before linting
1 parent 3ec44a6 commit 591e708

9 files changed

Lines changed: 295 additions & 2 deletions

.github/workflows/automated-tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ jobs:
2727
cache: npm
2828
- name: Install dependencies
2929
run: npm ci
30+
- name: Run unit tests
31+
run: node --run test:unit
3032
- name: Check spelling
3133
run: node --run test:spelling
3234
- name: Check linting

eslint.config.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ export default defineConfig([
7171
"sort-keys": "off"
7272
}
7373
},
74+
{
75+
files: ["tests/**/*.test.mjs"],
76+
languageOptions: {
77+
ecmaVersion: "latest",
78+
globals: {
79+
...globals.node
80+
},
81+
sourceType: "module"
82+
},
83+
plugins: { js, stylistic },
84+
extends: [importX.recommended, stylistic.configs.customize({ indent: "tab", quotes: "double", semi: true, commaDangle: "never" })],
85+
rules: {
86+
"@stylistic/indent": ["error", 2],
87+
"init-declarations": "off",
88+
"max-lines-per-function": "off",
89+
"max-statements": "off",
90+
"no-inline-comments": "off",
91+
"no-magic-numbers": "off",
92+
"no-undefined": "off"
93+
}
94+
},
7495
{ files: ["demo.config.js"], rules: { "prefer-const": "off" } },
7596
{ files: ["**/*.json"], ignores: ["package-lock.json"], plugins: { json }, extends: ["json/recommended"], language: "json/json" },
7697
{ files: ["**/*.md"], plugins: { markdown }, extends: ["markdown/recommended"], language: "markdown/gfm" }

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@
2929
"lint:fix": "eslint --fix && prettier . --write",
3030
"prepare": "node -e \"try{require('simple-git-hooks').run()}catch(e){}\"",
3131
"release": "commit-and-tag-version --commit-all --config ./changelog.config.js",
32-
"test": "node --run lint && node --run test:spelling",
33-
"test:spelling": "cspell ."
32+
"test": "node --run test:unit && node --run lint && node --run test:spelling",
33+
"test:spelling": "cspell .",
34+
"test:unit": "node --test tests/**/*.test.mjs"
3435
},
3536
"simple-git-hooks": {
3637
"pre-commit": "npx lint-staged"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, it } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { weatherModule } from "./test-setup.mjs";
4+
5+
describe("cardinalWindDirection", () => {
6+
it("should return N for 0 degrees", () => {
7+
assert.equal(weatherModule.cardinalWindDirection(0), "N");
8+
});
9+
10+
it("should return N for 360 degrees", () => {
11+
assert.equal(weatherModule.cardinalWindDirection(360), "N");
12+
});
13+
14+
it("should return NE for 45 degrees", () => {
15+
assert.equal(weatherModule.cardinalWindDirection(45), "NE");
16+
});
17+
18+
it("should return E for 90 degrees", () => {
19+
assert.equal(weatherModule.cardinalWindDirection(90), "E");
20+
});
21+
22+
it("should return SE for 135 degrees", () => {
23+
assert.equal(weatherModule.cardinalWindDirection(135), "SE");
24+
});
25+
26+
it("should return S for 180 degrees", () => {
27+
assert.equal(weatherModule.cardinalWindDirection(180), "S");
28+
});
29+
30+
it("should return SW for 225 degrees", () => {
31+
assert.equal(weatherModule.cardinalWindDirection(225), "SW");
32+
});
33+
34+
it("should return W for 270 degrees", () => {
35+
assert.equal(weatherModule.cardinalWindDirection(270), "W");
36+
});
37+
38+
it("should return NW for 315 degrees", () => {
39+
assert.equal(weatherModule.cardinalWindDirection(315), "NW");
40+
});
41+
42+
it("should handle intermediate values", () => {
43+
assert.equal(weatherModule.cardinalWindDirection(22), "NNE");
44+
assert.equal(weatherModule.cardinalWindDirection(67), "ENE");
45+
assert.equal(weatherModule.cardinalWindDirection(112), "ESE");
46+
assert.equal(weatherModule.cardinalWindDirection(157), "SSE");
47+
assert.equal(weatherModule.cardinalWindDirection(202), "SSW");
48+
assert.equal(weatherModule.cardinalWindDirection(247), "WSW");
49+
assert.equal(weatherModule.cardinalWindDirection(292), "WNW");
50+
assert.equal(weatherModule.cardinalWindDirection(337), "NNW");
51+
});
52+
53+
it("should handle boundary values", () => {
54+
assert.equal(weatherModule.cardinalWindDirection(11.24), "N");
55+
assert.equal(weatherModule.cardinalWindDirection(11.26), "NNE");
56+
assert.equal(weatherModule.cardinalWindDirection(348.74), "NNW");
57+
assert.equal(weatherModule.cardinalWindDirection(348.76), "N");
58+
});
59+
});

tests/convertWeatherType.test.mjs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, it } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { weatherModule } from "./test-setup.mjs";
4+
5+
describe("convertWeatherType", () => {
6+
it("should convert day icons correctly", () => {
7+
assert.equal(weatherModule.convertWeatherType("01d"), "day-clear-sky");
8+
assert.equal(weatherModule.convertWeatherType("02d"), "day-few-clouds");
9+
assert.equal(weatherModule.convertWeatherType("03d"), "day-scattered-clouds");
10+
assert.equal(weatherModule.convertWeatherType("04d"), "day-broken-clouds");
11+
assert.equal(weatherModule.convertWeatherType("09d"), "day-shower-rain");
12+
assert.equal(weatherModule.convertWeatherType("10d"), "day-rain");
13+
assert.equal(weatherModule.convertWeatherType("11d"), "day-thunderstorm");
14+
assert.equal(weatherModule.convertWeatherType("13d"), "day-snow");
15+
assert.equal(weatherModule.convertWeatherType("50d"), "day-mist");
16+
});
17+
18+
it("should convert night icons correctly", () => {
19+
assert.equal(weatherModule.convertWeatherType("01n"), "night-clear-sky");
20+
assert.equal(weatherModule.convertWeatherType("02n"), "night-few-clouds");
21+
assert.equal(weatherModule.convertWeatherType("03n"), "night-scattered-clouds");
22+
assert.equal(weatherModule.convertWeatherType("04n"), "night-broken-clouds");
23+
assert.equal(weatherModule.convertWeatherType("09n"), "night-shower-rain");
24+
assert.equal(weatherModule.convertWeatherType("10n"), "night-rain");
25+
assert.equal(weatherModule.convertWeatherType("11n"), "night-thunderstorm");
26+
assert.equal(weatherModule.convertWeatherType("13n"), "night-snow");
27+
assert.equal(weatherModule.convertWeatherType("50n"), "night-mist");
28+
});
29+
30+
it("should return null for unknown weather types", () => {
31+
assert.equal(weatherModule.convertWeatherType("99x"), null);
32+
assert.equal(weatherModule.convertWeatherType(""), null);
33+
assert.equal(weatherModule.convertWeatherType("invalid"), null);
34+
});
35+
36+
it("should handle undefined input", () => {
37+
assert.equal(weatherModule.convertWeatherType(undefined), null);
38+
});
39+
});

tests/getOrdinal.test.mjs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, it } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { weatherModule } from "./test-setup.mjs";
4+
5+
describe("getOrdinal", () => {
6+
it("should return N for 0 degrees", () => {
7+
assert.equal(weatherModule.getOrdinal(0), "N");
8+
});
9+
10+
it("should return N for 360 degrees", () => {
11+
assert.equal(weatherModule.getOrdinal(360), "N");
12+
});
13+
14+
it("should return NNE for 22.5 degrees", () => {
15+
assert.equal(weatherModule.getOrdinal(22.5), "NNE");
16+
});
17+
18+
it("should return NE for 45 degrees", () => {
19+
assert.equal(weatherModule.getOrdinal(45), "NE");
20+
});
21+
22+
it("should return E for 90 degrees", () => {
23+
assert.equal(weatherModule.getOrdinal(90), "E");
24+
});
25+
26+
it("should return SE for 135 degrees", () => {
27+
assert.equal(weatherModule.getOrdinal(135), "SE");
28+
});
29+
30+
it("should return S for 180 degrees", () => {
31+
assert.equal(weatherModule.getOrdinal(180), "S");
32+
});
33+
34+
it("should return SW for 225 degrees", () => {
35+
assert.equal(weatherModule.getOrdinal(225), "SW");
36+
});
37+
38+
it("should return W for 270 degrees", () => {
39+
assert.equal(weatherModule.getOrdinal(270), "W");
40+
});
41+
42+
it("should return NW for 315 degrees", () => {
43+
assert.equal(weatherModule.getOrdinal(315), "NW");
44+
});
45+
46+
it("should handle values between cardinal directions", () => {
47+
assert.equal(weatherModule.getOrdinal(11), "N");
48+
assert.equal(weatherModule.getOrdinal(12), "NNE");
49+
});
50+
51+
it("should handle large bearing values via modulo", () => {
52+
assert.equal(weatherModule.getOrdinal(720), "N"); // 720 % 360 = 0
53+
assert.equal(weatherModule.getOrdinal(405), "NE"); // 405 % 360 = 45
54+
});
55+
});

tests/mph2Beaufort.test.mjs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, it } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { weatherModule } from "./test-setup.mjs";
4+
5+
describe("mph2Beaufort", () => {
6+
it("should convert calm winds (0-1 mph) to Beaufort 0", () => {
7+
assert.equal(weatherModule.mph2Beaufort(0), 0);
8+
assert.equal(weatherModule.mph2Beaufort(0.5), 0);
9+
});
10+
11+
it("should convert light air (1-3 mph) to Beaufort 1", () => {
12+
assert.equal(weatherModule.mph2Beaufort(1), 1);
13+
assert.equal(weatherModule.mph2Beaufort(2), 1);
14+
});
15+
16+
it("should convert light breeze (4-7 mph) to Beaufort 2", () => {
17+
assert.equal(weatherModule.mph2Beaufort(4), 2);
18+
assert.equal(weatherModule.mph2Beaufort(6), 2);
19+
});
20+
21+
it("should convert gentle breeze (8-12 mph) to Beaufort 3", () => {
22+
assert.equal(weatherModule.mph2Beaufort(8), 3);
23+
assert.equal(weatherModule.mph2Beaufort(11), 3);
24+
});
25+
26+
it("should convert moderate breeze (13-18 mph) to Beaufort 4", () => {
27+
assert.equal(weatherModule.mph2Beaufort(13), 4);
28+
assert.equal(weatherModule.mph2Beaufort(17), 4);
29+
});
30+
31+
it("should convert hurricane force (73+ mph) to Beaufort 12", () => {
32+
assert.equal(weatherModule.mph2Beaufort(73), 12);
33+
assert.equal(weatherModule.mph2Beaufort(100), 12);
34+
assert.equal(weatherModule.mph2Beaufort(200), 12);
35+
});
36+
37+
it("should handle edge cases", () => {
38+
// Test boundary values
39+
assert.equal(weatherModule.mph2Beaufort(3.1), 1); // Still Beaufort 1
40+
assert.equal(weatherModule.mph2Beaufort(72), 11); // Just under hurricane force
41+
});
42+
});

tests/roundValue.test.mjs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { beforeEach, describe, it } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { weatherModule } from "./test-setup.mjs";
4+
5+
describe("roundValue", () => {
6+
beforeEach(() => {
7+
weatherModule.config = { roundTemp: false };
8+
});
9+
10+
it("should round to 1 decimal when roundTemp is false", () => {
11+
assert.equal(weatherModule.roundValue(23.456), "23.5");
12+
assert.equal(weatherModule.roundValue(23.444), "23.4");
13+
});
14+
15+
it("should round to integer when roundTemp is true", () => {
16+
weatherModule.config = { roundTemp: true };
17+
assert.equal(weatherModule.roundValue(23.456), "23");
18+
assert.equal(weatherModule.roundValue(23.6), "24");
19+
});
20+
21+
it("should handle negative temperatures", () => {
22+
weatherModule.config = { roundTemp: false };
23+
assert.equal(weatherModule.roundValue(-5.67), "-5.7");
24+
25+
weatherModule.config = { roundTemp: true };
26+
assert.equal(weatherModule.roundValue(-5.67), "-6");
27+
});
28+
29+
it("should handle zero", () => {
30+
weatherModule.config = { roundTemp: false };
31+
assert.equal(weatherModule.roundValue(0), "0.0");
32+
33+
weatherModule.config = { roundTemp: true };
34+
assert.equal(weatherModule.roundValue(0), "0");
35+
});
36+
});

tests/test-setup.mjs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Mock MagicMirror globals
2+
globalThis.config = { units: "metric" };
3+
4+
// Mock the Module.register to extract methods
5+
let weatherModule = null;
6+
7+
globalThis.Module = {
8+
register(name, definition) {
9+
weatherModule = definition;
10+
// Set config defaults for methods that need them
11+
weatherModule.config = {
12+
roundTemp: false,
13+
labelOrdinals: [
14+
"N",
15+
"NNE",
16+
"NE",
17+
"ENE",
18+
"E",
19+
"ESE",
20+
"SE",
21+
"SSE",
22+
"S",
23+
"SSW",
24+
"SW",
25+
"WSW",
26+
"W",
27+
"WNW",
28+
"NW",
29+
"NNW"
30+
]
31+
};
32+
}
33+
};
34+
35+
// Load the module
36+
await import("../MMM-OneCallWeather.js");
37+
38+
export { weatherModule };

0 commit comments

Comments
 (0)