4b949f7ce2
§12 «Математика вокруг нас»: задачи из жизни (покупки, сдача, измерения) + среднее значение. Финал главы: бой с 5 боссами (разряды, округление, сложение/вычитание, умножение, деление на дробь) с HP-баром; победа 4/5+ даёт +40 XP и достижение «Глава 1 пройдена». Эталонная Глава 1 готова: все 12 параграфов наполнены. Тесты 12/12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
149 lines
8.0 KiB
JavaScript
149 lines
8.0 KiB
JavaScript
'use strict';
|
||
/*
|
||
* Phase 0 jsdom-каркас «Математика 6»: хаб и 6 глав выполняются на движке
|
||
* math6_engine.js без ошибок скриптов; para-selector строится с нужным числом
|
||
* карточек; активен первый §; движок рендерит секции и заглушку/контент с кнопкой
|
||
* прочтения. Содержание §§ наполняется по главам — здесь проверяется каркас.
|
||
*/
|
||
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/math6_svg.js': readF('frontend/js/math6_svg.js'),
|
||
'/js/math6_engine.js': readF('frontend/js/math6_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: 'math_6_ch1.html', cards: 12 },
|
||
{ file: 'math_6_ch2.html', cards: 9 },
|
||
{ file: 'math_6_ch3.html', cards: 5 },
|
||
{ file: 'math_6_ch4.html', cards: 11 },
|
||
{ file: 'math_6_ch5.html', cards: 5 },
|
||
{ file: 'math_6_ch6.html', cards: 6 }
|
||
];
|
||
|
||
for (const ch of CHAPTERS) {
|
||
test(`${ch.file}: SPA без ошибок, ${ch.cards} карточек, активен § 1`, 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 === 'sec-p1', 'активен sec-p1');
|
||
const body = doc.querySelector('#p1-body');
|
||
assert.ok(body && body.children.length > 0, 'тело § 1 заполнено');
|
||
assert.ok(doc.querySelector('#p1-body [data-read]'), 'кнопка прочтения § 1');
|
||
/* финал помечен и присутствует */
|
||
assert.ok(doc.querySelector('#psel-grid .psel-card.final'), 'есть карточка финала');
|
||
});
|
||
}
|
||
|
||
test('hub: 6 карточек глав', async () => {
|
||
const { doc, errors } = await loadDom('math_6_hub.html');
|
||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||
assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 6, '6 глав');
|
||
});
|
||
|
||
test('ch1 Волна 1: интерактивы §1–§3 монтируются без ошибок', async () => {
|
||
const { doc, errors } = await loadDom('math_6_ch1.html');
|
||
const win = doc.defaultView;
|
||
// §1 строится при загрузке
|
||
assert.ok(doc.querySelector('#p1-iv1 #p1-c'), 'разрядный конструктор §1');
|
||
assert.ok(doc.querySelector('#p1-iv2 #p1-qq'), 'разряд-квиз §1');
|
||
win.goTo('p2'); await wait(80);
|
||
assert.ok(doc.querySelector('#p2-cfig svg'), 'числовая прямая сравнения §2');
|
||
assert.ok(doc.querySelector('#p2-iv2 #p2-rq'), 'тренажёр округления §2');
|
||
win.goTo('p3'); await wait(80);
|
||
assert.ok(doc.querySelector('#p3-afig svg'), 'координатный луч §3');
|
||
assert.ok(doc.querySelectorAll('#p3-iv2 [data-pt]').length === 4, 'выбор точки A–D §3');
|
||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||
});
|
||
|
||
test('ch1 Волна 2: интерактивы §4–§6 монтируются без ошибок', async () => {
|
||
const { doc, errors } = await loadDom('math_6_ch1.html');
|
||
const win = doc.defaultView;
|
||
win.goTo('p4'); await wait(80);
|
||
assert.ok(doc.querySelector('#p4-fig'), 'столбик §4');
|
||
assert.ok(doc.querySelectorAll('#p4-eopts').length === 1, 'варианты §4');
|
||
win.goTo('p5'); await wait(80);
|
||
assert.ok(doc.querySelector('#p5-iv1 [data-op]'), 'кнопки сдвига запятой §5');
|
||
assert.ok(doc.querySelector('#p5-out').textContent.length > 0 || doc.querySelector('#p5-out'), 'демонстратор §5');
|
||
win.goTo('p6'); await wait(80);
|
||
assert.ok(doc.querySelector('#p6-asl') && doc.querySelector('#p6-bsl'), 'ползунки множителей §6');
|
||
assert.ok(doc.querySelector('#p6-q'), 'тренажёр умножения §6');
|
||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||
});
|
||
|
||
test('ch1 Волна 3: интерактивы §7–§10 монтируются без ошибок', async () => {
|
||
const { doc, errors } = await loadDom('math_6_ch1.html');
|
||
const win = doc.defaultView;
|
||
win.goTo('p7'); await wait(80);
|
||
assert.ok(doc.querySelector('#p7-q') && doc.querySelector('#p7-rq'), 'тренажёры деления §7');
|
||
win.goTo('p8'); await wait(80);
|
||
assert.ok(doc.querySelector('#p8-pick [data-k]'), 'выбор примеров §8');
|
||
assert.ok(doc.querySelector('#p8-out').textContent.indexOf('=') >= 0, 'демонстратор переноса запятой §8');
|
||
win.goTo('p9'); await wait(80);
|
||
assert.ok(doc.querySelector('#p9-iv1 [data-fin]'), 'классификатор §9');
|
||
assert.ok(doc.querySelectorAll('#p9-dopts [data-o]').length === 3, 'варианты десятичной §9');
|
||
win.goTo('p10'); await wait(80);
|
||
assert.ok(doc.querySelectorAll('#p10-pool .dnd-chip').length === 5, 'сопоставление дробей §10');
|
||
assert.ok(doc.querySelector('#p10-q'), 'выражения §10');
|
||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||
});
|
||
|
||
test('ch1 Волна 4: §12 прикладной и финал-боссы', async () => {
|
||
const { doc, errors } = await loadDom('math_6_ch1.html');
|
||
const win = doc.defaultView;
|
||
win.goTo('app'); await wait(80);
|
||
assert.ok(doc.querySelector('#app-q') && doc.querySelector('#app-aq'), 'задачи §12');
|
||
win.goTo('final'); await wait(80);
|
||
assert.ok(doc.querySelector('#fin-q') && doc.querySelector('#fin-go'), 'арена боссов');
|
||
// финал на 100% → достижение ch1_done (finalAch)
|
||
win.bumpProgress('final', 100); await wait(20);
|
||
assert.ok(win.M6STATE.achievements.has('ch1_done'), 'достижение «Глава 1 пройдена» при финале');
|
||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||
});
|
||
|
||
test('навигация и прогресс: переход на § и отметка прочтения', async () => {
|
||
const { doc, errors } = await loadDom('math_6_ch1.html');
|
||
const win = doc.defaultView;
|
||
win.goTo('p4'); await wait(60);
|
||
assert.ok(doc.querySelector('#sec-p4.active'), 'перешли на § 4');
|
||
/* отметка прочтения начисляет XP */
|
||
const btn = doc.querySelector('#p4-body [data-read]');
|
||
assert.ok(btn, 'кнопка прочтения § 4');
|
||
btn.click(); await wait(20);
|
||
assert.ok((win.M6STATE.progress.p4 || 0) >= 30, 'прогресс § 4 вырос после прочтения');
|
||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||
});
|