f7d27ecb91
§4 Относительная атомная масса (весы атомов: во сколько раз тяжелее), §5 Молекулы и простые вещества (галерея молекул O2/O3/H2/N2 шариками), §6 Сложные вещества (классификатор простое/сложное + галерея H2O/CO2/CH4/NH3). Теория, тренажёры задач. Тест: 8/8 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
7.6 KiB
JavaScript
135 lines
7.6 KiB
JavaScript
'use strict';
|
|
/*
|
|
* Phase 0 jsdom-каркас «Химия 7»: проверяем, что хаб и 4 главы реально
|
|
* выполняются на движке chem8_engine.js без ошибок скриптов, строится
|
|
* para-selector с нужным числом карточек, активен первый §, заглушки-builder'ы
|
|
* рисуют para-hero и кнопку прочтения, а финал-боссы хаба решаются.
|
|
* Содержание § наполняется в фазах 1–4 — здесь проверяется только каркас.
|
|
*/
|
|
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));
|
|
|
|
/* Инлайним внешние скрипты главы (CDN убираем, api/xp заменяем заглушками). */
|
|
function buildPage(file) {
|
|
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'),
|
|
'/js/chem7_svg.js': readF('frontend/js/chem7_svg.js'),
|
|
'/js/chem7_ch1_widgets.js': readF('frontend/js/chem7_ch1_widgets.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) {
|
|
const errors = [];
|
|
const vc = new VirtualConsole();
|
|
vc.on('jsdomError', e => errors.push(e.message));
|
|
const dom = new JSDOM(buildPage(file), {
|
|
runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
|
|
beforeParse(w) { w.scrollTo = function () {}; }
|
|
});
|
|
await wait(160);
|
|
return { dom, errors, doc: dom.window.document };
|
|
}
|
|
|
|
const CHAPTERS = [
|
|
{ file: 'chemistry_7_ch1.html', cards: 15, first: 'sec-p1' },
|
|
{ file: 'chemistry_7_ch2.html', cards: 8, first: 'sec-p13' },
|
|
{ file: 'chemistry_7_ch3.html', cards: 9, first: 'sec-p18' },
|
|
{ file: 'chemistry_7_ch4.html', cards: 7, first: 'sec-p23' }
|
|
];
|
|
|
|
for (const ch of CHAPTERS) {
|
|
test(`${ch.file}: SPA без ошибок, ${ch.cards} карточек, активен ${ch.first}`, async () => {
|
|
const { doc, errors } = await loadDom(ch.file);
|
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
|
assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, ch.cards, ch.cards + ' карточек');
|
|
const active = doc.querySelector('.sec.active');
|
|
assert.ok(active && active.id === ch.first, 'активен ' + ch.first);
|
|
const firstId = ch.first.replace('sec-', '');
|
|
assert.ok(doc.querySelector('#' + firstId + '-body .para-hero'), 'para-hero первого §');
|
|
assert.ok(doc.querySelector('#' + firstId + '-body .read-wrap'), 'кнопка прочтения первого §');
|
|
});
|
|
}
|
|
|
|
test('ch1 Волна 1: интерактивы §1–§3 + ПР1 монтируются без ошибок', async () => {
|
|
const { doc, errors } = await loadDom('chemistry_7_ch1.html');
|
|
// §1 строится при загрузке (первый §) — классификатор «тело/вещество»
|
|
assert.ok(doc.querySelector('#p1-cls .c7-chip'), 'классификатор §1');
|
|
doc.defaultView.goTo('p2'); await wait(100);
|
|
assert.ok(doc.querySelector('#p2-sep .c7-m'), 'разделитель смесей §2');
|
|
doc.defaultView.goTo('pr1'); await wait(100);
|
|
assert.ok(doc.querySelector('#pr1-sep .c7-m'), 'разделитель смесей ПР1');
|
|
doc.defaultView.goTo('p3'); await wait(100);
|
|
assert.ok(doc.querySelectorAll('#p3-el .el-cell').length > 10, 'каталог элементов §3');
|
|
assert.ok(doc.querySelector('#p3-drill .c7-d'), 'тренажёр символов §3');
|
|
assert.ok(doc.querySelectorAll('#navDotsp3 .nav-dot').length >= 4, 'тренажёр задач §3');
|
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
|
});
|
|
|
|
test('ch1 Волна 2: интерактивы §4–§6 монтируются без ошибок', async () => {
|
|
const { doc, errors } = await loadDom('chemistry_7_ch1.html');
|
|
doc.defaultView.goTo('p4'); await wait(100);
|
|
assert.ok(doc.querySelector('#p4-bal #p4-a'), 'весы атомов §4');
|
|
doc.defaultView.goTo('p5'); await wait(100);
|
|
assert.ok(doc.querySelector('#p5-gal svg'), 'галерея молекул §5');
|
|
doc.defaultView.goTo('p6'); await wait(100);
|
|
assert.ok(doc.querySelector('#p6-cls .c7-chip'), 'классификатор простое/сложное §6');
|
|
assert.ok(doc.querySelector('#p6-gal svg'), 'галерея сложных веществ §6');
|
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
|
});
|
|
|
|
test('ch1: переход к §9 и финалу строит заглушку без ошибок', async () => {
|
|
const { doc, errors } = await loadDom('chemistry_7_ch1.html');
|
|
doc.defaultView.goTo('p9'); await wait(80);
|
|
assert.ok(doc.querySelector('#p9-body .para-hero'), 'para-hero §9');
|
|
doc.defaultView.goTo('final1'); await wait(80);
|
|
assert.ok(doc.querySelector('#final1-body .para-hero'), 'para-hero финала');
|
|
assert.equal(doc.querySelector('#final1-body .read-wrap'), null, 'у финала нет кнопки прочтения');
|
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
|
});
|
|
|
|
/* ── Хаб: каталог глав + финал курса ── */
|
|
function buildHub() {
|
|
let html = readF('frontend/textbooks/chemistry_7_hub.html');
|
|
return 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>/, '');
|
|
}
|
|
async function loadHub() {
|
|
const errors = []; const vc = new VirtualConsole(); vc.on('jsdomError', e => errors.push(e.message));
|
|
const dom = new JSDOM(buildHub(), { runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/', beforeParse(w){ w.scrollTo=function(){}; } });
|
|
await wait(60);
|
|
return { dom, errors, doc: dom.window.document };
|
|
}
|
|
|
|
test('hub: 4 карточки глав, финал курса — 8 боссов, босс решается', async () => {
|
|
const { doc, errors } = await loadHub();
|
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
|
assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 4, '4 карточки глав');
|
|
doc.getElementById('final-head').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
|
|
await wait(40);
|
|
assert.equal(doc.querySelectorAll('#fin-bosses-container .boss-card').length, 8, '8 боссов');
|
|
// решить босс 1 (Mr H2SO4 = 98)
|
|
const inp = doc.getElementById('fb-1-inp'), go = doc.getElementById('fb-1-go');
|
|
inp.value = '98'; go.dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
|
|
assert.ok(doc.getElementById('fb-1-card').classList.contains('solved'), 'босс 1 повержен');
|
|
});
|