From 6b927fc984bc2d94109305ec087bbf36aedd13d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20K=C3=BChn?= Date: Fri, 8 May 2026 16:59:29 +0200 Subject: [PATCH] Add spec for JS unit tests (goals.js + i18n.js) Co-Authored-By: Claude Sonnet 4.6 --- .../specs/2026-05-08-js-unit-tests-design.md | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-08-js-unit-tests-design.md diff --git a/docs/superpowers/specs/2026-05-08-js-unit-tests-design.md b/docs/superpowers/specs/2026-05-08-js-unit-tests-design.md new file mode 100644 index 0000000..fe4f929 --- /dev/null +++ b/docs/superpowers/specs/2026-05-08-js-unit-tests-design.md @@ -0,0 +1,125 @@ +# 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.