const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); function between(start, end) { const from = html.indexOf(start); const to = html.indexOf(end, from); assert.ok(from >= 0 && to > from, `Quellbereich fehlt: ${start} … ${end}`); return html.slice(from, to); } const core = between('const STYLES =', '/* ============================================================\n ZEITPLAN'); const simple = between('function applySimpleFermentation()', 'function initUI()'); const warnings = between('function renderWarnings(', 'function renderRecipe('); const fixedNow = 1_700_000_000_000; class FixedDate extends Date { static now() { return fixedNow; } } const warningHost = { innerHTML: '' }; const sandbox = { Date: FixedDate, console, $: id => id === 'warns' ? warningHost : {}, pct: x => `${x} %`, n1: x => String(x), }; vm.createContext(sandbox); vm.runInContext(`${core}\n${simple}\n${warnings}\nglobalThis.api={STYLES,DEFAULT,calc,applySimpleFermentation,renderWarnings,setState:s=>S=s,getState:()=>S};`, sandbox); const { STYLES, DEFAULT, calc, applySimpleFermentation, renderWarnings, setState, getState } = sandbox.api; function state(overrides = {}) { return { ...DEFAULT, pf: 'none', yeast: 'fresh', balls: 4, ball: 250, mode: 'w', ...overrides }; } // Napoletana und Contemporanea werden bewusst niedriger kalibriert. assert.equal(STYLES.napo.yeastK, 1.5); assert.equal(STYLES.contemp.yeastK, 1.7); // 8 h warme Napoletana bei 20 °C: rund 0,19 % Frischhefe statt bisher 0,76 %. const napo8 = calc(state({ style: 'napo', seq: 'room', roomH: 8, roomT: 20, fridgeH: 0 })); assert.ok(Math.abs(napo8.yPct - 0.190875) < 1e-9, `unerwartet: ${napo8.yPct}%`); assert.ok(napo8.main.yeast > 1.1 && napo8.main.yeast < 1.2, `unerwartet: ${napo8.main.yeast} g`); // Nicht neu kalibrierte Stile behalten den bisherigen Referenzfaktor 6. const ny8 = calc(state({ style: 'ny', salt: 2, seq: 'room', roomH: 8, roomT: 20, fridgeH: 0 })); assert.equal(ny8.yPct, 0.75); // Lange Führung im Einfach-Modus endet mit 4 h warmer Stückgare. setState(state({ uxMode: 'simple', bake: fixedNow + 24.75 * 3600e3 })); applySimpleFermentation(); assert.equal(getState().seq, 'coldwarm'); assert.equal(getState().roomH, 4); assert.equal(getState().fridgeH, 20); setState(state({ uxMode: 'simple', bake: fixedNow + 48.75 * 3600e3 })); applySimpleFermentation(); assert.equal(getState().roomH, 4); assert.equal(getState().fridgeH, 44); // Unter 4 h wird Napoletana ausdrücklich als Schnellteig gekennzeichnet. const napo3State = state({ style: 'napo', seq: 'room', roomH: 3, roomT: 20, fridgeH: 0 }); setState(napo3State); const napo3 = calc(napo3State); renderWarnings({ n: 'Testmehl', reife: [0, 120], hyd: [50, 90], vt: true }, napo3); assert.match(warningHost.innerHTML, /Schnellteig/); // Kleine absolute Hefemengen nennen die Feinwaage, auch wenn der Prozentwert über 0,03 % liegt. const napo24State = state({ style: 'napo', seq: 'room', roomH: 24, roomT: 20, fridgeH: 0 }); setState(napo24State); const napo24 = calc(napo24State); assert.ok(napo24.main.yeast < 0.5 && napo24.yPct > 0.03); renderWarnings({ n: 'Testmehl', reife: [0, 120], hyd: [50, 90], vt: true }, napo24); assert.match(warningHost.innerHTML, /Feinwaage/); console.log('yeast calibration regression tests: OK');