809d0316c3
feat(chemistry-8): перестройка раздела intro под эталон учебников (SPA-движок) По замечанию: учебник не соответствовал структуре/наполнению других учебников. Перестроено по контракту глав физики (para-selector SPA + движок задач): - chem8_engine.js — общий движок: para-selector, ленивая сборка §, makeCard, тренажёр задач (числовой ввод + MCQ, nav-dots, score), sidebar-шпаргалка с XP, уровни/достижения, серверная синхронизация прогресса, тема. Конфиг — CHEM8_CFG. - chem8-textbook.css — фреймворк-CSS: layout+sidebar, hero, psel-карточки, para-hero (9 градиентов), карточки теории, def/remember/insight, тренажёр, mcq, флагман-карточки, виджеты, ach-popup (amber-палитра). - chem8_intro_widgets.js — виджеты § (карта элементов, Mr, порция, Авогадро, M+объём) и флагманы (треугольник n–m–M, калькулятор газа, балансировщик, пошаговый решатель) на chem8_svg.js. - chemistry_8_intro.html — перестроен: PARAS, build_p1..p9+pr1+final, POOLS (38 задач), SIDEBARS, TIPS. Богатая анатомия § как в физике. Тесты: 23/23 (юнит + jsdom-виджеты + полностраничный jsdom SPA — para-selector, активный §, монтаж виджетов, тренажёр, без ошибок скриптов). Ассеты отдаются 200. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
84 lines
4.2 KiB
JavaScript
84 lines
4.2 KiB
JavaScript
'use strict';
|
|
/*
|
|
* Полностраничная jsdom-проверка chemistry_8_intro.html (SPA на chem8_engine.js):
|
|
* выполняем реальный HTML + движок + виджеты, даём таймерам отработать, проверяем,
|
|
* что para-selector построен, первый § активен и виджеты смонтированы — без ошибок.
|
|
*/
|
|
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));
|
|
|
|
function buildPage() {
|
|
let html = readF('frontend/textbooks/chemistry_8_intro.html');
|
|
const inl = {
|
|
'/js/biochem-core.js': readF('frontend/js/biochem-core.js'),
|
|
'/js/chem8_svg.js': readF('frontend/js/chem8_svg.js'),
|
|
'/js/chem8_intro_widgets.js': readF('frontend/js/chem8_intro_widgets.js'),
|
|
'/js/chem8_engine.js': readF('frontend/js/chem8_engine.js')
|
|
};
|
|
// CDN katex → удалить; api/xp → стабы (LS отсутствует, renderMathInElement — no-op)
|
|
html = html
|
|
.replace(/<script defer src="https:\/\/cdn[^"]*"[^>]*><\/script>/g, '')
|
|
.replace(/<script src="\/js\/api\.js" defer><\/script>/, '<script>window.renderMathInElement=function(){};</script>')
|
|
.replace(/<script src="\/js\/xp\.js" defer><\/script>/, '');
|
|
Object.keys(inl).forEach(src => {
|
|
// функция-замена: иначе $-последовательности в коде ломают вставку
|
|
html = html.replace(new RegExp('<script src="' + src + '" defer><\\/script>'), () => '<script>\n' + inl[src] + '\n</script>');
|
|
});
|
|
return html;
|
|
}
|
|
|
|
async function loadDom() {
|
|
const errors = [];
|
|
const vc = new VirtualConsole();
|
|
vc.on('jsdomError', e => errors.push(e.message));
|
|
const dom = new JSDOM(buildPage(), {
|
|
runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
|
|
beforeParse(w) { w.scrollTo = function () {}; } // jsdom не реализует scrollTo (в браузере есть)
|
|
});
|
|
await wait(180); // дать отработать таймерам сборки § и монтажа виджетов (40–50 мс)
|
|
return { dom, errors, doc: dom.window.document };
|
|
}
|
|
|
|
test('страница SPA выполняется без ошибок скриптов', async () => {
|
|
const { errors } = await loadDom();
|
|
assert.deepEqual(errors, [], 'нет jsdomError: ' + errors.join(' | '));
|
|
});
|
|
|
|
test('para-selector построен (11 карточек) и первый § активен', async () => {
|
|
const { doc } = await loadDom();
|
|
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 11, '11 карточек §');
|
|
const active = doc.querySelector('.sec.active');
|
|
assert.ok(active && active.id === 'sec-p1', 'активен §1');
|
|
assert.ok(doc.querySelector('#p1-body .para-hero'), 'para-hero §1 построен');
|
|
});
|
|
|
|
test('виджеты § смонтированы движком', async () => {
|
|
const { doc } = await loadDom();
|
|
assert.ok(doc.querySelectorAll('#p1-el .el-cell').length > 10, 'карта элементов §1');
|
|
// перейдём на §6 и §8 через goTo, дождёмся монтажа флагманов
|
|
doc.defaultView.goTo('p6'); await wait(120);
|
|
assert.ok(doc.querySelector('#p6-mount .mtri'), 'треугольник §6');
|
|
doc.defaultView.goTo('p8'); await wait(120);
|
|
assert.ok(doc.querySelector('#p8-mount .ceqb'), 'балансировщик §8');
|
|
});
|
|
|
|
test('тренажёр задач отрисован для §2 (POOLS)', async () => {
|
|
const { doc } = await loadDom();
|
|
doc.defaultView.goTo('p2'); await wait(150);
|
|
assert.ok(doc.querySelector('#taskArea p2, #taskAreap2'), 'область задач §2');
|
|
assert.ok(doc.querySelectorAll('#navDotsp2 .nav-dot').length >= 4, 'навигация по задачам §2');
|
|
});
|
|
|
|
test('Chem8 доступен и считает Mr', async () => {
|
|
const { dom } = await loadDom();
|
|
assert.ok(dom.window.Chem8, 'window.Chem8 определён');
|
|
assert.equal(dom.window.Chem8.molarMass('CaCO3'), 100);
|
|
});
|