126 lines
4.6 KiB
Markdown
126 lines
4.6 KiB
Markdown
|
|
# 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.
|