# 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: ```js 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: ```js // 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: ```js 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: ```js // 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 ```bash 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: ```json { "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.