test(biochem): регресс-тесты химического ядра (node --test)
backend/tests/biochem-core.test.js — 8 тестов BIO (window-shim): формулы, VSEPR-геометрия (вода/метан/CO2 + углы), частичные заряды, полярность, балансировщик, SMILES-парсер, analyze. Все проходят. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
'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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user