4.6 KiB
JS Unit Tests Design
Goal
Add Node.js unit tests for public/js/goals.js (pure math functions) and public/js/i18n.js (translation/locale helpers) using the built-in node:test runner — no npm, no build step.
Architecture
Two test files under tests/js/, run with node --test tests/js/.
tests/js/
test-i18n.js tests for tr(), ldoc(), setLocale(), applyLocale()
test-goals.js tests for heuteColor(), dTot(), o2d(), tOff(), calc(), dcls(), dlbl()
Both files use node:test and node:assert/strict. No test framework, no npm.
Browser Global Stubs
i18n.js calls localStorage.setItem in setLocale(..., true) and localStorage.getItem in applyLocale(undefined). state.js calls localStorage.getItem in loadPrefs() — but that function wraps the call in try/catch, so it survives without a stub. For clarity and to test all branches, both files add minimal stubs at the top:
globalThis.localStorage = {
_data: {},
getItem(k) { return this._data[k] ?? null; },
setItem(k, v) { this._data[k] = v; },
};
globalThis.navigator = { language: 'de' };
Import Strategy
test-goals.js must set up the globals before state.js is evaluated. Static import declarations hoist and run before any code in the file, so test-goals.js uses top-level await import() instead:
// test-goals.js — top of file (no static imports for project modules)
globalThis.localStorage = { ... };
const { state } = await import('../../public/js/state.js');
const { calc, heuteColor, ... } = await import('../../public/js/goals.js');
test-i18n.js can use static imports because i18n.js has zero imports and accesses no globals at module evaluation time.
State Control for goals.js
tOff(g) subtracts g.start from state.TODAY. All tests that depend on date arithmetic set state.TODAY explicitly:
state.TODAY = new Date(2026, 4, 8, 0, 0, 0, 0); // May 8, 2026 local midnight
state.collapsed and state.goals are reset between tests that mutate them.
Test Cases
test-i18n.js (~15 cases)
| Function | Case |
|---|---|
tr() |
returns German string for 'de' |
tr() |
returns English string for 'en' |
tr() |
returns Polish string for 'pl' |
tr() |
falls back to German for unknown locale |
tr() |
returns key for completely unknown key |
ldoc() |
de → 'de-DE' |
ldoc() |
en → 'en-GB' |
ldoc() |
pl → 'pl-PL' |
setLocale('en', false) |
changes locale, no localStorage write |
setLocale('en', true) |
writes to localStorage |
applyLocale('pl') |
explicit valid lang sets locale |
applyLocale('xx') |
unknown lang leaves locale unchanged |
applyLocale(null) |
reads localStorage, falls back to navigator |
test-goals.js (~20 cases)
| Function | Case |
|---|---|
heuteColor(0, 10) |
red (nothing done) |
heuteColor(5, 10) |
amber (partial) |
heuteColor(10, 10) |
green (exact) |
heuteColor(11, 10) |
blue (over by 10%) |
dTot(g, 0) |
sums amounts from sets objects |
dTot(g, 0) |
empty set returns 0 |
dTot(g, 99) |
missing key returns 0 |
o2d(g, 0) |
day 0 = start date at midnight |
o2d(g, 1) |
day 1 = start + 1 day at midnight |
tOff(g) |
7 days after start = 7 |
calc(g) |
day 0, nothing done: pct=0, pd=daily, ok=false |
calc(g) |
day 7, all past days met, today partial: pd=daily, no deficit |
calc(g) |
day 7, one missed day: pd > daily, deficit < 0 |
calc(g) |
pct capped at 100 |
dcls(g, i) |
future day → 'dot df' |
dcls(g, i) |
missed day → 'dot dm dl' |
dcls(g, i) |
today (editable) with partial → contains ' de' |
dlbl(g, i) |
future day → day number as string |
dlbl(g, i) |
missed → '✕' |
dlbl(g, i) |
exact → '✓' |
dlbl(g, i) |
partial → percentage string |
dlbl(g, i) |
over 110% → '+' |
sets Format
g.sets is keyed by day offset as string (not date string), values are arrays of {amount: number} objects:
// 10 reps on day 0, 5 reps on day 1
sets: { '0': [{ amount: 10 }], '1': [{ amount: 5 }] }
g.start format from the API: 'YYYY-MM-DD HH:MM:SS' (e.g. '2026-05-01 00:00:00').
Run Command
node --test tests/js/
All test files use ES module syntax (import). Node resolves .js files as CommonJS by default, so a root-level package.json must be added:
{ "type": "module" }
This is a new file — the project has no existing package.json. It is safe to add: the deploy script uses only git and composer, and the browser ignores it. Without this, state.js would fail to parse as ESM when dynamically imported by the tests.