dudi/docs/superpowers/specs/2026-05-08-js-unit-tests-design.md
Simon Kühn 6b927fc984 Add spec for JS unit tests (goals.js + i18n.js)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 16:59:29 +02:00

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.