@
feat(chemistry-8): Phase 2 — Глава 1 «Важнейшие классы неорг. соединений» (§10–23) Полная глава на движке (14 § + 2 лаб. опыта + 2 практические работы + финал-босс): - §10–12 оксиды (классификатор, свойства, получение) - §13–15 кислоты (классификатор, ряд активности, индикаторы, получение) - §16–18 основания (классификатор, фенолфталеин, Лаб.1 Cu(OH)₂↓, ПР2 нейтрализация) - §19–21 соли (таблица растворимости, РИО, соль+металл, Лаб.2, способы) - §22 генетическая связь классов + ПР3; §23 расчётный решатель; финал-босс (6 задач) - POOLS: ~45 задач (MCQ + числовые), шпаргалки и подсказки по каждому § chem8_svg.js: реализованы 5 хим-виджетов (были заглушки) — testTube (осадок/газ), indicatorScale (лакмус/фенолфталеин/метилоранж + pH), classifier (клик-DnD), solubilityTable (катион×анион), activitySeries (ряд активности металлов). chem8-textbook.css: стили виджетов. chem8_ch1_widgets.js: монтаж по §. Тесты: 24/24 (юнит + jsdom-виджеты + полностраничный SPA intro и ch1 — para-selector, активный §, монтаж флагманов, тренажёр, без ошибок). Ассеты 200. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Полностраничная jsdom-проверка chemistry_8_intro.html (SPA на chem8_engine.js):
|
||||
* выполняем реальный HTML + движок + виджеты, даём таймерам отработать, проверяем,
|
||||
* что para-selector построен, первый § активен и виджеты смонтированы — без ошибок.
|
||||
* Полностраничная jsdom-проверка глав «Химия 8» (SPA на chem8_engine.js):
|
||||
* выполняем реальный HTML + движок + виджеты, даём таймерам отработать, проверяем
|
||||
* para-selector, активный §, монтаж виджетов — без ошибок скриптов.
|
||||
*/
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
@@ -14,70 +14,70 @@ 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');
|
||||
function buildPage(file, widgetsSrc) {
|
||||
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/chem8_intro_widgets.js': readF('frontend/js/chem8_intro_widgets.js'),
|
||||
[widgetsSrc]: readF('frontend/js' + widgetsSrc.replace('/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() {
|
||||
async function loadDom(file, widgetsSrc) {
|
||||
const errors = [];
|
||||
const vc = new VirtualConsole();
|
||||
vc.on('jsdomError', e => errors.push(e.message));
|
||||
const dom = new JSDOM(buildPage(), {
|
||||
const dom = new JSDOM(buildPage(file, widgetsSrc), {
|
||||
runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
|
||||
beforeParse(w) { w.scrollTo = function () {}; } // jsdom не реализует scrollTo (в браузере есть)
|
||||
beforeParse(w) { w.scrollTo = function () {}; }
|
||||
});
|
||||
await wait(180); // дать отработать таймерам сборки § и монтажа виджетов (40–50 мс)
|
||||
await wait(180);
|
||||
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, дождёмся монтажа флагманов
|
||||
/* ── Вводный раздел ── */
|
||||
test('intro: SPA без ошибок, 11 карточек, §1 активен, виджеты', async () => {
|
||||
const { doc, errors } = await loadDom('chemistry_8_intro.html', '/js/chem8_intro_widgets.js');
|
||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 11, '11 карточек');
|
||||
assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p1', '§1 активен');
|
||||
assert.ok(doc.querySelectorAll('#p1-el .el-cell').length > 10, 'карта элементов');
|
||||
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');
|
||||
/* ── Глава 1 ── */
|
||||
test('ch1: SPA без ошибок, 15 карточек, §10 активен', async () => {
|
||||
const { doc, errors } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js');
|
||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 15, '14 § + финал');
|
||||
assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p10', '§10 активен');
|
||||
assert.ok(doc.querySelector('#p10-body .para-hero'), 'para-hero §10');
|
||||
});
|
||||
|
||||
test('Chem8 доступен и считает Mr', async () => {
|
||||
const { dom } = await loadDom();
|
||||
assert.ok(dom.window.Chem8, 'window.Chem8 определён');
|
||||
assert.equal(dom.window.Chem8.molarMass('CaCO3'), 100);
|
||||
test('ch1: флагман-виджеты монтируются (классификатор, растворимость, ряд активности)', async () => {
|
||||
const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js');
|
||||
doc.defaultView.goTo('p10'); await wait(120);
|
||||
assert.ok(doc.querySelector('#c-ox-cls .cls-chip'), 'классификатор оксидов §10');
|
||||
doc.defaultView.goTo('p13'); await wait(120);
|
||||
assert.ok(doc.querySelector('#c-acid-ind .ind-strip'), 'индикатор §13');
|
||||
doc.defaultView.goTo('p19'); await wait(120);
|
||||
assert.ok(doc.querySelector('#c-salt-sol .sol-tab'), 'таблица растворимости §19');
|
||||
doc.defaultView.goTo('p14'); await wait(120);
|
||||
assert.ok(doc.querySelector('#c-acid-act .act-cell'), 'ряд активности §14');
|
||||
});
|
||||
|
||||
test('ch1: тренажёр задач отрисован для §10', async () => {
|
||||
const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js');
|
||||
await wait(150);
|
||||
assert.ok(doc.querySelectorAll('#navDotsp10 .nav-dot').length >= 4, 'навигация по задачам §10');
|
||||
});
|
||||
|
||||
@@ -64,13 +64,20 @@ test('Chem8.elementCounts — скобки и индексы', () => {
|
||||
assert.deepEqual(C.elementCounts('CO2'), { C: 1, O: 2 });
|
||||
});
|
||||
|
||||
test('Chem8 — заглушки возвращают null и не падают', () => {
|
||||
for (const fn of ['testTube', 'solubilityTable', 'oxStateCalc', 'geneticMap']) {
|
||||
test('Chem8 — оставшиеся заглушки возвращают null и не падают', () => {
|
||||
for (const fn of ['oxStateCalc', 'redoxBalancer', 'orbitalDiagram', 'miniPeriodic', 'dissociationAnim', 'geneticMap']) {
|
||||
assert.equal(typeof C[fn], 'function', fn + ' определён');
|
||||
assert.equal(C[fn]({}), null, fn + ' заглушка возвращает null');
|
||||
}
|
||||
});
|
||||
|
||||
test('Chem8 — Phase 2 виджеты экспортированы как функции', () => {
|
||||
for (const fn of ['testTube', 'indicatorScale', 'classifier', 'solubilityTable', 'activitySeries']) {
|
||||
assert.equal(typeof C[fn], 'function', fn + ' реализован');
|
||||
}
|
||||
assert.ok(C.testTube({ precipitate: '#88c' }).includes('<svg'), 'testTube → SVG');
|
||||
});
|
||||
|
||||
test('Chem8 — движки расчётов экспортированы как функции', () => {
|
||||
for (const fn of ['moleTriangle', 'equationBalancer']) {
|
||||
assert.equal(typeof C[fn], 'function', fn + ' определён');
|
||||
@@ -106,10 +113,10 @@ test('каждая глава существует, ссылается на ха
|
||||
const html = fs.readFileSync(path.join(TB, ch.file), 'utf8');
|
||||
assert.ok(html.includes('/textbook/chemistry-8"'), ch.file + ' ссылка назад в хаб');
|
||||
assert.ok(html.includes('/js/chem8_svg.js'), ch.file + ' подключает chem8_svg');
|
||||
if (ch.slug === 'chemistry-8-intro') {
|
||||
// intro перестроен на движок (SPA): slug задаётся через CHEM8_CFG
|
||||
assert.ok(html.includes("slug:'chemistry-8-intro'"), 'intro slug в CHEM8_CFG');
|
||||
assert.ok(html.includes('/js/chem8_engine.js'), 'intro подключает движок');
|
||||
if (ch.slug === 'chemistry-8-intro' || ch.slug === 'chemistry-8-ch1') {
|
||||
// перестроены на движок (SPA): slug задаётся через CHEM8_CFG
|
||||
assert.ok(html.includes("slug:'" + ch.slug + "'"), ch.file + ' slug в CHEM8_CFG');
|
||||
assert.ok(html.includes('/js/chem8_engine.js'), ch.file + ' подключает движок');
|
||||
} else {
|
||||
assert.ok(html.includes("const _TB_SLUG = '" + ch.slug + "'"), ch.file + ' slug (каркас)');
|
||||
}
|
||||
@@ -130,6 +137,19 @@ test('Phase 1 — раздел intro перестроен на движок (SPA
|
||||
assert.ok(!html.includes('Раздел в разработке'), 'баннер-заглушка убран');
|
||||
});
|
||||
|
||||
test('Phase 2 — Глава 1 построена на движке (§10–23 + лаб/ПР + финал)', () => {
|
||||
const html = fs.readFileSync(path.join(TB, 'chemistry_8_ch1.html'), 'utf8');
|
||||
assert.ok(html.includes('id="psel-grid"'), 'para-selector');
|
||||
for (let i = 10; i <= 23; i++) assert.ok(html.includes('id="sec-p' + i + '"'), '§' + i + ' секция');
|
||||
assert.ok(html.includes('id="sec-final1"'), 'финал');
|
||||
assert.ok(html.includes('id="c-ox-cls"'), 'классификатор оксидов');
|
||||
assert.ok(html.includes('id="c-salt-sol"'), 'таблица растворимости');
|
||||
assert.ok(html.includes('Лабораторный опыт 1'), 'Лаб.1');
|
||||
assert.ok(html.includes('Практическая работа 2'), 'ПР2');
|
||||
assert.ok(html.includes('/js/chem8_ch1_widgets.js'), 'виджеты главы');
|
||||
assert.ok(!html.includes('Раздел в разработке'), 'заглушка убрана');
|
||||
});
|
||||
|
||||
test('chem8_engine.js и виджеты — валидный синтаксис', () => {
|
||||
const eng = fs.readFileSync(path.join(ROOT, 'frontend', 'js', 'chem8_engine.js'), 'utf8');
|
||||
const wid = fs.readFileSync(path.join(ROOT, 'frontend', 'js', 'chem8_intro_widgets.js'), 'utf8');
|
||||
|
||||
Reference in New Issue
Block a user