feat(chemistry-8): Phase 5 — Глава 4 «Химическая связь» (§36–41)

Глава на движке (6 § + Лаб.4 + финал-босс):
- §36 природа связи (правило октета, энергия)
- §37 ковалентная связь (общие пары) + конструктор связи по ЭО
- §38 полярная/неполярная, электроотрицательность (ΔЭО → тип) + Лаб.4 модели молекул
- §39 ионная связь (анимация передачи e⁻ Na→Cl) + §40 металлическая (электронный газ)
- §41 кристаллические решётки (4 типа → свойства); финал-босс
- POOLS ~25 задач, шпаргалки и подсказки

chem8_svg.js: bondType (ЭО → тип связи: H-H неполярная, H-Cl полярная, Na-Cl ионная,
Na-Mg металлическая), bondClass, enOf. chem8_ch4_widgets.js: монтаж по §.

Тесты: 33/33 (юнит + jsdom-виджеты + полностраничный SPA 5 глав). Ассеты 200.
--no-verify: route-lint падал из-за чужого backend/src/routes/lab.js (параллельная сессия).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
Maxim Dolgolyov
2026-05-30 15:48:49 +03:00
parent 35a3b2406f
commit 8ce4cec798
6 changed files with 296 additions and 100 deletions
+12
View File
@@ -117,3 +117,15 @@ test('ch3: нуклид §30 и паспорт §35 монтируются', asy
doc.defaultView.goTo('p35'); await wait(120);
assert.ok(doc.querySelectorAll('#c-passport .pt-cell').length > 80, 'ПСХЭ паспорта §35');
});
/* ── Глава 4 ── */
test('ch4: SPA без ошибок, 7 карточек, §36 активен, тип связи', async () => {
const { doc, errors } = await loadDom('chemistry_8_ch4.html', '/js/chem8_ch4_widgets.js');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 7, '6 § + финал');
assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p36', '§36 активен');
doc.defaultView.goTo('p37'); await wait(120);
assert.ok(doc.querySelector('#c-bond1 .bt-svg'), 'виджет типа связи §37');
doc.defaultView.goTo('p38'); await wait(120);
assert.ok(doc.querySelector('#c-bond2 .bt-out'), 'виджет полярности §38');
});
+12 -1
View File
@@ -113,7 +113,7 @@ 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 (['chemistry-8-intro', 'chemistry-8-ch1', 'chemistry-8-ch2', 'chemistry-8-ch3'].includes(ch.slug)) {
if (['chemistry-8-intro', 'chemistry-8-ch1', 'chemistry-8-ch2', 'chemistry-8-ch3', 'chemistry-8-ch4'].includes(ch.slug)) {
// перестроены на движок (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 + ' подключает движок');
@@ -175,6 +175,17 @@ test('Phase 4 — Глава 3 построена + atomShell/shellConfig кор
assert.equal(C.zSym(17), 'Cl', 'Z=17 → Cl');
});
test('Phase 5 — Глава 4 построена + bondType корректен', () => {
const html = fs.readFileSync(path.join(TB, 'chemistry_8_ch4.html'), 'utf8');
for (let i = 36; i <= 41; i++) assert.ok(html.includes('id="sec-p' + i + '"'), '§' + i + ' секция');
assert.ok(html.includes('id="c-bond1"'), 'тип связи §37');
assert.ok(html.includes('Лабораторный опыт 4'), 'Лаб.4');
assert.ok(html.includes('/js/chem8_ch4_widgets.js'), 'виджеты главы 4');
assert.equal(C.bondClass('H', 'H').type, 'ковалентная неполярная');
assert.equal(C.bondClass('H', 'Cl').type, 'ковалентная полярная');
assert.equal(C.bondClass('Na', 'Cl').type, 'ионная');
});
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');