Files
Learn_System/backend/tests/biochem-core.test.js
T
Maxim Dolgolyov 177a5b94d7 feat(biochem): Фазы 2-7 — химдвижок, баланс, энергодиаграммы, графики, SMILES
Перенос изолированной работы по модулю «Биохимия» на master (разработка
велась параллельно с другой сессией; здесь только biochem-файлы).

Ядро biochem-core.js:
- Фаза 2 (химдвижок): partialCharges (по ЭО), dipole (вектор q·r по 3D VSEPR),
  polarity, massFractions, functionalGroups, analyze; chargeColor + δ± в рендерах.
- Фаза 3: balance() — балансировка уравнений (матрица элементов + дробный Гаусс).
- Фаза 7: parseSmiles (учебное подмножество) + toJSON/download.
- Фикс 3D-рендера: глубинная сортировка + объёмные связи-цилиндры.

Страницы:
- biochem.html: δ±-тепловая карта зарядов + стрелка диполя; импорт SMILES;
  экспорт PNG/JSON; замена крудных эвристик на BIO.analyze (−95 строк).
- biochem-reactions.html: энергопрофиль реакции + проверка баланса.
- biochem-properties.html: график молярных масс + экспорт CSV.

Тесты: backend/tests/biochem-core.test.js (8/8 pass: формулы, VSEPR, заряды,
полярность, баланс, SMILES, analyze).

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

85 lines
3.7 KiB
JavaScript

'use strict';
/*
* Регресс-тесты химического ядра frontend/js/biochem-core.js (window.BIO).
* Чистые функции (формулы, VSEPR, заряды, диполь, баланс, SMILES) — без DOM.
*/
const test = require('node:test');
const assert = require('node:assert');
const path = require('node:path');
// shim browser global, then load the frontend module
global.window = global;
require(path.join(__dirname, '..', '..', 'frontend', 'js', 'biochem-core.js'));
const B = global.BIO;
const m = (atoms, bonds) => ({ atoms, bonds });
const water = m([{ id: 1, s: 'O' }, { id: 2, s: 'H' }, { id: 3, s: 'H' }], [{ f: 1, t: 2, o: 1 }, { f: 1, t: 3, o: 1 }]);
const co2 = m([{ id: 1, s: 'C' }, { id: 2, s: 'O' }, { id: 3, s: 'O' }], [{ f: 1, t: 2, o: 2 }, { f: 1, t: 3, o: 2 }]);
const methane = m([{ id: 1, s: 'C' }, { id: 2, s: 'H' }, { id: 3, s: 'H' }, { id: 4, s: 'H' }, { id: 5, s: 'H' }],
[{ f: 1, t: 2, o: 1 }, { f: 1, t: 3, o: 1 }, { f: 1, t: 4, o: 1 }, { f: 1, t: 5, o: 1 }]);
function bondAngle(a3, c, n1, n2) {
const C = a3.find(a => a.id === c), P = a3.find(a => a.id === n1), Q = a3.find(a => a.id === n2);
const v1 = [P.x - C.x, P.y - C.y, P.z - C.z], v2 = [Q.x - C.x, Q.y - C.y, Q.z - C.z];
const d = v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
return Math.acos(d / (Math.hypot(...v1) * Math.hypot(...v2))) * 180 / Math.PI;
}
test('hillFormula & molarMass', () => {
assert.equal(B.hillFormula(methane.atoms), 'CH4');
assert.ok(Math.abs(B.molarMass(methane.atoms) - 16.04) < 0.05);
});
test('parseFormula handles parentheses', () => {
assert.deepEqual(B.parseFormula('Ca(OH)2'), { Ca: 1, O: 2, H: 2 });
});
test('VSEPR geometry: water bent, methane tetrahedral, CO2 linear', () => {
const gw = B.vsepr(water.atoms, water.bonds);
assert.equal(gw.shape, 'угловая');
assert.equal(gw.hybridization, 'sp³');
const gm = B.vsepr(methane.atoms, methane.bonds);
assert.equal(gm.shape, 'тетраэдрическая');
assert.ok(Math.abs(bondAngle(gm.atoms3d, 1, 2, 3) - 109.5) < 1.5);
const gc = B.vsepr(co2.atoms, co2.bonds);
assert.equal(gc.shape, 'линейная');
assert.ok(Math.abs(bondAngle(gc.atoms3d, 1, 2, 3) - 180) < 1);
});
test('partial charges: water O negative, H positive', () => {
const q = B.partialCharges(water.atoms, water.bonds);
assert.ok(q[1] < -0.3, 'O should be δ−');
assert.ok(q[2] > 0.1 && q[3] > 0.1, 'H should be δ+');
});
test('polarity: symmetric molecules nonpolar, water polar', () => {
assert.equal(B.polarity(co2.atoms, co2.bonds).label, 'Неполярная');
assert.equal(B.polarity(methane.atoms, methane.bonds).label, 'Неполярная');
assert.ok(B.polarity(water.atoms, water.bonds).dipole > 0.3);
});
test('balance: classic equations', () => {
assert.deepEqual(B.balance(['H2', 'O2'], ['H2O']).coefficients, [2, 1, 2]);
assert.deepEqual(B.balance(['CH4', 'O2'], ['CO2', 'H2O']).coefficients, [1, 2, 1, 2]);
assert.deepEqual(B.balance(['Fe', 'O2'], ['Fe2O3']).coefficients, [4, 3, 2]);
assert.deepEqual(B.balance(['Ca(OH)2', 'HCl'], ['CaCl2', 'H2O']).coefficients, [1, 2, 1, 2]);
});
test('parseSmiles: skeleton + implicit H', () => {
assert.equal(B.hillFormula(B.parseSmiles('CCO').atoms), 'C2H6O');
assert.equal(B.hillFormula(B.parseSmiles('CC(=O)O').atoms), 'C2H4O2');
assert.equal(B.hillFormula(B.parseSmiles('C1=CC=CC=C1').atoms), 'C6H6');
assert.equal(B.hillFormula(B.parseSmiles('ClC(Cl)(Cl)Cl').atoms), 'CCl4');
assert.equal(B.parseSmiles('bad[x]'), null);
});
test('analyze returns a complete report', () => {
const a = B.analyze(water.atoms, water.bonds);
assert.equal(a.formula, 'H2O');
assert.ok(a.geometry && a.geometry.shape === 'угловая');
assert.ok(a.dipole > 0);
assert.ok(a.massFractions.O > 80);
});