Add spec for JS unit tests (goals.js + i18n.js)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon Kühn 2026-05-08 16:59:29 +02:00
parent 157559e9aa
commit 6b927fc984

View file

@ -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.