'use strict'; /* * Phase 0 тесты учебника «Химия 8» (hub + 7 глав). * 1. Чистые примитивы frontend/js/chem8_svg.js (window.Chem8): formula/ionLabel/chemEq. * 2. Целостность каркаса: хаб + 7 файлов глав существуют, slug'и согласованы, * сумма параграфов = 52, миграция 041 содержит родителя + 7 детей. */ const test = require('node:test'); const assert = require('node:assert'); const fs = require('node:fs'); const path = require('node:path'); const ROOT = path.join(__dirname, '..', '..'); const TB = path.join(ROOT, 'frontend', 'textbooks'); // --- shim browser global, load the frontend module --- global.window = global; require(path.join(ROOT, 'frontend', 'js', 'chem8_svg.js')); const C = global.Chem8; test('Chem8.formula — числовые индексы в подстрочные', () => { assert.equal(C.formula('CaCO3'), 'CaCO₃'); assert.equal(C.formula('H2O'), 'H₂O'); assert.equal(C.formula('Al2(SO4)3'), 'Al₂(SO₄)₃'); assert.equal(C.formula('NaCl'), 'NaCl'); }); test('Chem8.ionLabel — заряд ионов надстрочным', () => { assert.equal(C.ionLabel('Na', 1), 'Na⁺'); assert.equal(C.ionLabel('Ca', 2), 'Ca²⁺'); assert.equal(C.ionLabel('Cl', -1), 'Cl⁻'); assert.equal(C.ionLabel('SO4', -2), 'SO₄²⁻'); assert.equal(C.ionLabel('Fe', 3), 'Fe³⁺'); assert.equal(C.ionLabel('Na', 0), 'Na'); }); test('Chem8.chemEq — стрелка, индексы, газ', () => { const html = C.chemEq('2Na + 2H2O -> 2NaOH + H2^'); assert.ok(html.includes('2H₂O'), 'индексы воды'); assert.ok(html.includes('→'), 'стрелка реакции'); assert.ok(html.includes('H₂↑'), 'значок газа'); assert.ok(html.includes('class="ceq"'), 'обёртка'); }); test('Chem8.chemEq — обратимая реакция и осадок', () => { const rev = C.chemEq('N2 + 3H2 <-> 2NH3'); assert.ok(rev.includes('⇌'), 'обратимая стрелка'); const prec = C.chemEq('AgNO3 + NaCl -> AgClv + NaNO3'); assert.ok(prec.includes('AgCl↓'), 'значок осадка'); }); test('Chem8 — заглушки возвращают null и не падают', () => { for (const fn of ['testTube', 'moleTriangle', 'solubilityTable', 'oxStateCalc', 'geneticMap']) { assert.equal(typeof C[fn], 'function', fn + ' определён'); assert.equal(C[fn]({}), null, fn + ' заглушка возвращает null'); } }); // --- каркас страниц --- const CHILDREN = [ { slug: 'chemistry-8-intro', file: 'chemistry_8_intro.html', paras: 9 }, { slug: 'chemistry-8-ch1', file: 'chemistry_8_ch1.html', paras: 14 }, { slug: 'chemistry-8-ch2', file: 'chemistry_8_ch2.html', paras: 5 }, { slug: 'chemistry-8-ch3', file: 'chemistry_8_ch3.html', paras: 7 }, { slug: 'chemistry-8-ch4', file: 'chemistry_8_ch4.html', paras: 6 }, { slug: 'chemistry-8-ch5', file: 'chemistry_8_ch5.html', paras: 4 }, { slug: 'chemistry-8-ch6', file: 'chemistry_8_ch6.html', paras: 7 } ]; test('сумма параграфов глав = 52', () => { assert.equal(CHILDREN.reduce((a, c) => a + c.paras, 0), 52); }); test('хаб chemistry_8_hub.html существует и ссылается на все 7 глав', () => { const hub = fs.readFileSync(path.join(TB, 'chemistry_8_hub.html'), 'utf8'); assert.ok(hub.includes('var TOTAL = 52'), 'TOTAL=52'); for (const ch of CHILDREN) { assert.ok(hub.includes('/textbook/' + ch.slug), 'ссылка на ' + ch.slug); } assert.ok(hub.includes('/api/textbooks/chemistry-8/children'), 'грузит детей'); }); test('каждая глава существует и задаёт свой _TB_SLUG', () => { for (const ch of CHILDREN) { const html = fs.readFileSync(path.join(TB, ch.file), 'utf8'); assert.ok(html.includes("const _TB_SLUG = '" + ch.slug + "'"), ch.file + ' slug'); assert.ok(html.includes('/textbook/chemistry-8"'), ch.file + ' ссылка назад в хаб'); assert.ok(html.includes('/js/chem8_svg.js'), ch.file + ' подключает chem8_svg'); assert.ok(html.includes('/js/biochem-core.js'), ch.file + ' подключает biochem-core'); } }); test('миграция 041 — родитель chemistry-8 + 7 детей, нет эмоджи', () => { const sql = fs.readFileSync( path.join(ROOT, 'backend', 'src', 'db', 'migrations', '041_chemistry8_hub.sql'), 'utf8'); assert.ok(/'chemistry-8'.*NULL/s.test(sql) || sql.includes("'chemistry-8', 'chemistry', 8"), 'родитель'); for (const ch of CHILDREN) { assert.ok(sql.includes("'" + ch.slug + "'"), 'дитя ' + ch.slug); } // запрет эмоджи (правило проекта) assert.ok(!/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}]/u.test(sql), 'без эмоджи'); });