From fe378371bd0d82bf9be49a3abe965172ff171cb5 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 20:58:05 +0300 Subject: [PATCH] =?UTF-8?q?fix(math6):=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D1=8C=20init()=20=D0=BF=D0=BE=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D1=85=D0=B5=D0=BB=D0=BF=D0=B5=D1=80=D0=BE=D0=B2=20=D0=B2=20?= =?UTF-8?q?window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реальная причина пустых §1 (заглушки) во всех главах: в math6_engine.js вызов init() стоял ВЫШЕ строк window.makeCard=…/secNav=…. При обычной загрузке через defer скрипт исполняется при readyState='interactive', поэтому ветка `else init()` срабатывала синхронно — init→goTo→buildP1() звал makeCard ДО его экспорта → ReferenceError 'makeCard is not defined' → перехват в ensureBuilt → заглушка. В jsdom-тестах баг не воспроизводился (там старт шёл через DOMContentLoaded, экспорты успевали). - init() теперь вызывается СТРОГО после всех window.* экспортов. - ensureBuilt перечитывает window.M6 (надёжнее против устаревшего замыкания). - html учебника всегда no-store (убрал кэш-причину стале-страниц). - регресс-тест: init() обязан идти после window.makeCard. Тесты 18/18. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/math6-page.test.js | 8 ++++++++ frontend/js/math6_engine.js | 15 ++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js index 52b035f..fc0b97f 100644 --- a/backend/tests/math6-page.test.js +++ b/backend/tests/math6-page.test.js @@ -53,6 +53,14 @@ const CHAPTERS = [ { file: 'math_6_ch6.html', cards: 6 } ]; +test('engine: init() вызывается ПОСЛЕ экспортов (guard от sync-defer бага makeCard)', () => { + const src = readF('frontend/js/math6_engine.js'); + const exportIdx = src.indexOf('window.makeCard = makeCard'); + const initCallIdx = src.lastIndexOf('else init();'); + assert.ok(exportIdx > 0, 'есть экспорт window.makeCard'); + assert.ok(initCallIdx > exportIdx, 'else init() должен идти ПОСЛЕ window.makeCard = makeCard (иначе билдеры упадут с ReferenceError при defer-старте)'); +}); + for (const ch of CHAPTERS) { test(`${ch.file}: SPA без ошибок, ${ch.cards} карточек, активен § 1`, async () => { const { doc, errors } = await loadDom(ch.file); diff --git a/frontend/js/math6_engine.js b/frontend/js/math6_engine.js index 1d82f37..2982aa9 100644 --- a/frontend/js/math6_engine.js +++ b/frontend/js/math6_engine.js @@ -165,10 +165,11 @@ function buildParaSelector() { var BUILT = new Set(); function ensureBuilt(id) { if (BUILT.has(id)) return; - var fn = M6.builders && M6.builders[id]; + BUILT.add(id); + var cfg = window.M6 || M6; /* читаем актуальный конфиг из window */ + var fn = cfg.builders && cfg.builders[id]; if (fn) { try { fn(); } catch (e) { placeholder(id); } } else placeholder(id); - BUILT.add(id); } function placeholder(id) { var box = document.getElementById(id + '-body'); if (!box) return; @@ -368,9 +369,6 @@ function init() { window.LS.xp.load().then(function (s) { if (s && s.xp > STATE.xp) { STATE.xp = s.xp; STATE.level = calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if (STATE.current) buildSidebar(STATE.current); } }).catch(function () {}); } } -if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); -else init(); - /* ============================================================ EXPORTS (для inline-билдеров) */ window.goTo = goTo; window.makeCard = makeCard; @@ -386,4 +384,11 @@ window.setupSorter = setupSorter; window.confetti = confetti; window.M6icon = M6icon; window.M6engine = { goTo: goTo, ensureBuilt: ensureBuilt, refreshProgressUI: refreshProgressUI, buildSidebar: buildSidebar }; + +/* Запуск init — СТРОГО ПОСЛЕ экспортов в window. Иначе при defer-старте + (readyState='interactive') синхронная ветка else init() вызовет билдеры, + которые обращаются к makeCard/secNav/feedback ДО их экспорта → ReferenceError + → перехват в ensureBuilt → заглушка. */ +if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); +else init(); })();