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:
Maxim Dolgolyov
2026-05-30 15:20:13 +03:00
parent b9d30f5252
commit 787092674a
6 changed files with 740 additions and 163 deletions
+26 -6
View File
@@ -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');