Files
Learn_System/backend/tests/math5-page.test.js
T
Maxim Dolgolyov c020a2c948 feat(math5): Phase 0 — фундамент учебника «Математика 5»
План (PLAN_MATH_5 + VISUAL: карта 22 визуал-компонентов), миграция
050_math5_hub (хаб math-5 + 3 главы: Натуральные числа §1–17, Выражения.
Уравнения §1–9, Обыкновенные дроби §1–18), страница-хаб (3 карточки +
курсовой финал из 3 боссов + звание «Математик 5 класса») и 3 каркаса глав
на ОБЩЕМ движке math6 (window.M6 с slug math-5-chN, ключи math5_*).
Baseline-тест math5-page: 6/6. § без билдера → заглушка движка.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:09:42 +03:00

98 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/*
* Phase 0 jsdom-каркас «Математика 5»: хаб и 3 главы выполняются на ОБЩЕМ движке
* math6_engine.js (учебник 5 класса переиспользует тот же движок/SVG/anim через
* собственный window.M6 с slug 'math-5-chN'). Проверяется: страницы грузятся без
* ошибок скриптов, para-selector строится с нужным числом карточек, активен § 1,
* заглушка с кнопкой прочтения на месте, финал помечен. Содержание §§ наполняется
* по главам отдельными билдерами — здесь проверяется фундамент.
*/
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_anim.js': readF('frontend/js/math6_anim.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_5_ch1.html', cards: 18 },
{ file: 'math_5_ch2.html', cards: 10 },
{ file: 'math_5_ch3.html', cards: 19 }
];
test('engine: init() вызывается ПОСЛЕ экспортов (общий движок math6 — guard от sync-defer бага)', () => {
const src = readF('frontend/js/math6_engine.js');
const exportIdx = src.indexOf('window.makeCard = makeCard');
const initCallIdx = src.lastIndexOf('else init();');
assert.ok(exportIdx > 0, 'есть экспорт window.makeCard');
assert.ok(initCallIdx > exportIdx, 'else init() должен идти ПОСЛЕ window.makeCard = makeCard');
});
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 .m6-placeholder'), 'заглушка § 1 (нет билдера — ожидаемо в каркасе)');
assert.ok(doc.querySelector('#p1-body [data-read]'), 'кнопка прочтения § 1');
assert.ok(doc.querySelector('#psel-grid .psel-card.final'), 'есть карточка финала');
});
}
test('хаб math-5: 3 главы, курсовой финал, ачивка-полоса', async () => {
const { doc, errors } = await loadDom('math_5_hub.html');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 3, '3 карточки глав');
assert.ok(doc.querySelector('a[href="/textbook/math-5-ch1"]'), 'ссылка на главу 1');
assert.ok(doc.querySelector('a[href="/textbook/math-5-ch3"]'), 'ссылка на главу 3');
assert.ok(doc.querySelector('#cf-go') && doc.querySelector('#cf-q'), 'арена курсового финала');
assert.ok(doc.querySelector('#ach-strip'), 'полоса звания «Математик 5 класса»');
});
test('движок 5 класса: прогресс/XP считаются на math5_*-ключах', async () => {
const { doc } = await loadDom('math_5_ch1.html');
const win = doc.defaultView;
assert.ok(win.M6 && win.M6.slug === 'math-5-ch1', 'M6.slug = math-5-ch1');
assert.equal(win.M6.lsPrefix, 'math5_ch1', 'lsPrefix = math5_ch1');
assert.equal(win.M6.xpKey, 'math5_xp', 'xpKey = math5_xp');
win.bumpProgress('final', 100); await wait(20);
assert.ok(win.M6STATE.achievements.has('ch1_done'), 'достижение «Глава 1 пройдена» при финале 100%');
});