'use strict'; /* * Phase 0 jsdom-каркас «Химия 7»: проверяем, что хаб и 4 главы реально * выполняются на движке chem8_engine.js без ошибок скриптов, строится * para-selector с нужным числом карточек, активен первый §, заглушки-builder'ы * рисуют para-hero и кнопку прочтения, а финал-боссы хаба решаются. * Содержание § наполняется в фазах 1–4 — здесь проверяется только каркас. */ const test = require('node:test'); const assert = require('node:assert'); const fs = require('node:fs'); const path = require('node:path'); const { JSDOM, VirtualConsole } = require('jsdom'); const ROOT = path.join(__dirname, '..', '..'); const readF = p => fs.readFileSync(path.join(ROOT, p), 'utf8'); const wait = ms => new Promise(r => setTimeout(r, ms)); /* Инлайним внешние скрипты главы (CDN убираем, api/xp заменяем заглушками). */ function buildPage(file) { let html = readF('frontend/textbooks/' + file); const inl = { '/js/biochem-core.js': readF('frontend/js/biochem-core.js'), '/js/chem8_svg.js': readF('frontend/js/chem8_svg.js'), '/js/chem7_svg.js': readF('frontend/js/chem7_svg.js'), '/js/chem7_ch1_widgets.js': readF('frontend/js/chem7_ch1_widgets.js'), '/js/chem7_ch2_widgets.js': readF('frontend/js/chem7_ch2_widgets.js'), '/js/chem8_engine.js': readF('frontend/js/chem8_engine.js') }; html = html .replace(/') .replace(/'); }); return html; } async function loadDom(file) { const errors = []; const vc = new VirtualConsole(); vc.on('jsdomError', e => errors.push(e.message)); const dom = new JSDOM(buildPage(file), { runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/', beforeParse(w) { w.scrollTo = function () {}; } }); await wait(160); return { dom, errors, doc: dom.window.document }; } const CHAPTERS = [ { file: 'chemistry_7_ch1.html', cards: 15, first: 'sec-p1' }, { file: 'chemistry_7_ch2.html', cards: 8, first: 'sec-p13' }, { file: 'chemistry_7_ch3.html', cards: 9, first: 'sec-p18' }, { file: 'chemistry_7_ch4.html', cards: 7, first: 'sec-p23' } ]; for (const ch of CHAPTERS) { test(`${ch.file}: SPA без ошибок, ${ch.cards} карточек, активен ${ch.first}`, async () => { const { doc, errors } = await loadDom(ch.file); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, ch.cards, ch.cards + ' карточек'); const active = doc.querySelector('.sec.active'); assert.ok(active && active.id === ch.first, 'активен ' + ch.first); const firstId = ch.first.replace('sec-', ''); assert.ok(doc.querySelector('#' + firstId + '-body .para-hero'), 'para-hero первого §'); assert.ok(doc.querySelector('#' + firstId + '-body .read-wrap'), 'кнопка прочтения первого §'); }); } test('ch1 Волна 1: интерактивы §1–§3 + ПР1 монтируются без ошибок', async () => { const { doc, errors } = await loadDom('chemistry_7_ch1.html'); // §1 строится при загрузке (первый §) — классификатор «тело/вещество» assert.ok(doc.querySelector('#p1-cls .c7-chip'), 'классификатор §1'); doc.defaultView.goTo('p2'); await wait(100); assert.ok(doc.querySelector('#p2-sep .c7-m'), 'разделитель смесей §2'); doc.defaultView.goTo('pr1'); await wait(100); assert.ok(doc.querySelector('#pr1-sep .c7-m'), 'разделитель смесей ПР1'); doc.defaultView.goTo('p3'); await wait(100); assert.ok(doc.querySelectorAll('#p3-el .el-cell').length > 10, 'каталог элементов §3'); assert.ok(doc.querySelector('#p3-drill .c7-d'), 'тренажёр символов §3'); assert.ok(doc.querySelectorAll('#navDotsp3 .nav-dot').length >= 4, 'тренажёр задач §3'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); test('ch1 Волна 2: интерактивы §4–§6 монтируются без ошибок', async () => { const { doc, errors } = await loadDom('chemistry_7_ch1.html'); doc.defaultView.goTo('p4'); await wait(100); assert.ok(doc.querySelector('#p4-bal #p4-a'), 'весы атомов §4'); doc.defaultView.goTo('p5'); await wait(100); assert.ok(doc.querySelector('#p5-gal svg'), 'галерея молекул §5'); doc.defaultView.goTo('p6'); await wait(100); assert.ok(doc.querySelector('#p6-cls .c7-chip'), 'классификатор простое/сложное §6'); assert.ok(doc.querySelector('#p6-gal svg'), 'галерея сложных веществ §6'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); test('ch1 Волна 3: интерактивы §7–§9 монтируются и считают', async () => { const { doc, errors } = await loadDom('chemistry_7_ch1.html'); doc.defaultView.goTo('p7'); await wait(100); assert.ok(doc.querySelector('#p7-out'), 'парсер формулы §7'); assert.match(doc.querySelector('#p7-out').textContent, /4/, 'H2SO4 → 4 атома O в разборе'); doc.defaultView.goTo('p8'); await wait(100); assert.match(doc.querySelector('#p8-out').textContent, /100/, 'M_r(CaCO3)=100'); doc.defaultView.goTo('p9'); await wait(100); assert.ok(doc.querySelector('#p9-bld #p9-a'), 'конструктор валентности §9'); assert.match(doc.querySelector('#p9-bout').textContent, /Al/, 'формула по валентности построена'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); test('ch1 Волна 4: §10–§12 + ЛО1 + финал главы монтируются', async () => { const { doc, errors } = await loadDom('chemistry_7_ch1.html'); doc.defaultView.goTo('p10'); await wait(100); assert.ok(doc.querySelector('#p10-signs #p10-signs-go'), 'детектор признаков §10'); doc.defaultView.goTo('lo1'); await wait(100); assert.ok(doc.querySelector('#lo1-signs #lo1-signs-go'), 'детектор признаков ЛО1'); doc.defaultView.goTo('p11'); await wait(100); assert.ok(doc.querySelector('#p11-bal svg'), 'весы сохранения массы §11'); doc.defaultView.goTo('p12'); await wait(120); assert.ok(doc.querySelector('#p12-mount').childElementCount > 0, 'балансировщик §12'); doc.defaultView.goTo('final1'); await wait(120); assert.ok(doc.querySelectorAll('#navDotsfinal1 .nav-dot').length >= 6, 'боссы финала главы'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); test('ch1: переход к §9 и финалу строит без ошибок', async () => { const { doc, errors } = await loadDom('chemistry_7_ch1.html'); doc.defaultView.goTo('p9'); await wait(80); assert.ok(doc.querySelector('#p9-body .para-hero'), 'para-hero §9'); doc.defaultView.goTo('final1'); await wait(80); assert.ok(doc.querySelector('#final1-body .para-hero'), 'para-hero финала'); assert.equal(doc.querySelector('#final1-body .read-wrap'), null, 'у финала нет кнопки прочтения'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); test('ch2 Волна 1: интерактивы §13 + ЛО2 + §14 + §15 монтируются', async () => { const { doc, errors } = await loadDom('chemistry_7_ch2.html'); assert.ok(doc.querySelector('#p13-air .air-seg'), 'диаграмма состава воздуха §13'); doc.defaultView.goTo('lo2'); await wait(100); assert.ok(doc.querySelector('#lo2-coll #lo2-pick'), 'выбор собирания газа ЛО2'); doc.defaultView.goTo('p14'); await wait(100); assert.ok(doc.querySelector('#p14-tog #p14-o2'), 'переключатель элемент/вещество §14'); doc.defaultView.goTo('p15'); await wait(100); assert.ok(doc.querySelector('#p15-burn #p15-go'), 'симулятор горения §15'); doc.defaultView.goTo('p15'); doc.getElementById('p15-go').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); assert.match(doc.querySelector('#p15-out').textContent, /оксид/, 'горение даёт оксид'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); test('ch2 Волна 2: §16 + §17 + ПР2 + финал главы монтируются', async () => { const { doc, errors } = await loadDom('chemistry_7_ch2.html'); doc.defaultView.goTo('p16'); await wait(100); assert.ok(doc.querySelector('#p16-bld #p16-el'), 'конструктор оксида §16'); assert.ok(doc.querySelector('#p16-cls .c7-chip'), 'классификатор оксид/не оксид §16'); doc.defaultView.goTo('p17'); await wait(100); assert.ok(doc.querySelector('#p17-prod #p17-pick'), 'схема получения O2 §17'); doc.defaultView.goTo('pr2'); await wait(100); assert.ok(doc.querySelector('#pr2-test #pr2-go'), 'проверка кислорода ПР2'); doc.defaultView.goTo('final2'); await wait(120); assert.ok(doc.querySelectorAll('#navDotsfinal2 .nav-dot').length >= 6, 'боссы финала главы 2'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); /* ── Хаб: каталог глав + финал курса ── */ function buildHub() { let html = readF('frontend/textbooks/chemistry_7_hub.html'); return html .replace(/') .replace(/