8ce4cec798
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> @
132 lines
7.5 KiB
JavaScript
132 lines
7.5 KiB
JavaScript
'use strict';
|
|
/*
|
|
* Полностраничная jsdom-проверка глав «Химия 8» (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(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'),
|
|
[widgetsSrc]: readF('frontend/js' + widgetsSrc.replace('/js', '')),
|
|
'/js/chem8_engine.js': readF('frontend/js/chem8_engine.js')
|
|
};
|
|
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(file, widgetsSrc) {
|
|
const errors = [];
|
|
const vc = new VirtualConsole();
|
|
vc.on('jsdomError', e => errors.push(e.message));
|
|
const dom = new JSDOM(buildPage(file, widgetsSrc), {
|
|
runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
|
|
beforeParse(w) { w.scrollTo = function () {}; }
|
|
});
|
|
await wait(180);
|
|
return { dom, errors, doc: dom.window.document };
|
|
}
|
|
|
|
/* ── Вводный раздел ── */
|
|
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');
|
|
});
|
|
|
|
/* ── Глава 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('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');
|
|
});
|
|
|
|
/* ── Глава 2 ── */
|
|
test('ch2: SPA без ошибок, 6 карточек, §24 активен, ПСХЭ', async () => {
|
|
const { doc, errors } = await loadDom('chemistry_8_ch2.html', '/js/chem8_ch2_widgets.js');
|
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
|
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 6, '5 § + финал');
|
|
assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p24', '§24 активен');
|
|
await wait(120);
|
|
assert.ok(doc.querySelectorAll('#c-pt-metals .pt-cell').length > 80, 'ПСХЭ §24 (90 элементов)');
|
|
});
|
|
|
|
test('ch2: амфотерность §25 и семейства §26 монтируются', async () => {
|
|
const { doc } = await loadDom('chemistry_8_ch2.html', '/js/chem8_ch2_widgets.js');
|
|
doc.defaultView.goTo('p25'); await wait(120);
|
|
assert.ok(doc.querySelector('#c-amph .amph-btn'), 'амфотерность §25');
|
|
doc.defaultView.goTo('p26'); await wait(120);
|
|
assert.ok(doc.querySelectorAll('#c-pt-fam .pt-cell').length > 80, 'ПСХЭ семейства §26');
|
|
});
|
|
|
|
/* ── Глава 3 ── */
|
|
test('ch3: SPA без ошибок, 8 карточек, §29 активен, модель атома', async () => {
|
|
const { doc, errors } = await loadDom('chemistry_8_ch3.html', '/js/chem8_ch3_widgets.js');
|
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
|
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 8, '7 § + финал');
|
|
assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p29', '§29 активен');
|
|
await wait(120);
|
|
assert.ok(doc.querySelector('#c-atom .as-svg'), 'модель атома §29');
|
|
});
|
|
|
|
test('ch3: нуклид §30 и паспорт §35 монтируются', async () => {
|
|
const { doc } = await loadDom('chemistry_8_ch3.html', '/js/chem8_ch3_widgets.js');
|
|
doc.defaultView.goTo('p30'); await wait(120);
|
|
assert.ok(doc.querySelector('#c-nuclide #nz'), 'калькулятор нуклида §30');
|
|
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');
|
|
});
|