Files
Learn_System/backend/tests/chemistry8-dom.test.js
T
Maxim Dolgolyov 6ea140af54 @
feat(chemistry-8): Phase 1 — раздел «Количественные понятия» (§1–9 + ПР1)

Полноценная интерактивная страница chemistry_8_intro.html (9 § + ПР1 + босс):
- §1 карта элементов (Z, название, Ar), §2 калькулятор Mr по формуле
- §3 «порция вещества» n⇒N,m, §4 счётчик частиц N=n·N_A, §5 M+молярный объём
- §6 звёздный виджет: интерактивный треугольник n–m–M
- §7 универсальный калькулятор газа (m–n–V–N), §8 балансировщик уравнений
- §9 пошаговый решатель по уравнению; босс раздела (4 задачи) + ачивка «Счёт в химии»
- прогресс/XP через /api/textbooks/chemistry-8-intro/progress, scrollspy, тема

chem8_svg.js: реализованы движки — molarMass (школьные Ar: Mr(H2O)=18),
elementCounts, moleTriangle, equationBalancer (+ fmt, arOf).

Фикс порядка загрузки: инициализация обёрнута в DOMContentLoaded (defer-скрипты
готовы к этому моменту). Генератор каркасов получил skip-if-exists (--force для перезаписи).

Тесты: chemistry8.test.js (14) + chemistry8-dom.test.js (jsdom-смоук виджетов, 3) — 17/17.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-05-30 14:36:31 +03:00

64 lines
3.1 KiB
JavaScript

'use strict';
/*
* jsdom-смоук виджетов chem8_svg.js: реальная отрисовка в DOM, ввод, проверка.
* Ловит рантайм-ошибки DOM-манипуляций, которые не видны в чистых юнит-тестах.
*/
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const { JSDOM } = require('jsdom');
const SRC = fs.readFileSync(
path.join(__dirname, '..', '..', 'frontend', 'js', 'chem8_svg.js'), 'utf8');
function mkDom() {
const dom = new JSDOM('<!DOCTYPE html><body><div id="m"></div><div id="b"></div></body>');
// выполняем модуль так, что его `window` === jsdom-окно
new Function('window', SRC)(dom.window);
return { dom, C: dom.window.Chem8, doc: dom.window.document };
}
function fire(el, type) {
el.dispatchEvent(new el.ownerDocument.defaultView.Event(type, { bubbles: true }));
}
test('moleTriangle монтируется и считает m = n·M', () => {
const { C, doc } = mkDom();
const api = C.moleTriangle(doc.getElementById('m'), {});
assert.ok(api && api.el, 'виджет смонтирован');
const inputs = doc.querySelectorAll('#m input[data-k]');
assert.equal(inputs.length, 3, '3 поля');
const byKey = {};
inputs.forEach(i => { byKey[i.getAttribute('data-k')] = i; });
// вводим n=2, затем M=18 → ожидаем m=36
byKey.n.value = '2'; fire(byKey.n, 'input');
byKey.M.value = '18'; fire(byKey.M, 'input');
const out = doc.querySelector('#m [data-out]');
assert.ok(/36/.test(out.textContent), 'm = 36 вычислено: ' + out.textContent);
});
test('equationBalancer: неверные коэффициенты → дисбаланс, верные → баланс', () => {
const { C, doc } = mkDom();
const api = C.equationBalancer(doc.getElementById('b'),
{ skeleton: 'H2 + O2 -> H2O', solution: [2, 1, 2] });
assert.ok(api && api.check, 'виджет смонтирован');
// по умолчанию все коэффициенты = 1 → не сбалансировано
assert.equal(api.check(), false, '1·H2 + 1·O2 -> 1·H2O не сбалансировано');
const out = doc.querySelector('#b [data-out]');
assert.ok(out.className.includes('bad'), 'подсветка дисбаланса');
// применяем решение через кнопку
doc.querySelector('#b [data-solve]').dispatchEvent(
new doc.defaultView.Event('click', { bubbles: true }));
assert.ok(out.className.includes('ok'), 'после решения — сбалансировано: ' + out.className);
});
test('equationBalancer считает атомы для сложной реакции', () => {
const { C, doc } = mkDom();
const api = C.equationBalancer(doc.getElementById('b'),
{ skeleton: 'Al + HCl -> AlCl3 + H2', solution: [2, 6, 2, 3] });
const coefs = doc.querySelectorAll('#b .ceqb-coef');
[2, 6, 2, 3].forEach((v, i) => { coefs[i].value = String(v); });
assert.equal(api.check(), true, '2Al + 6HCl -> 2AlCl3 + 3H2 сбалансировано');
});