Files
Learn_System/backend/tests/math5-page.test.js
T
Maxim Dolgolyov a4a0ae1a77 feat(math5): Глава 1 §10–§12 — степень, деление с остатком, НОД и НОК
§10 Степень (a^n, основание/показатель; квадрат из клеток a×a + тренажёр степеней).
§11 Деление с остатком (a=bq+r; точки по b в ряд, остаток красным + тренажёр
неполного частного). §12 Делители/кратные, НОД/НОК (делители-чипсы с подсветкой
общих → НОД + тренажёр НОК). Шпаргалки/типсы §10–12. Тесты math5: 8/8.

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

141 lines
8.0 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 [data-read]'), 'кнопка прочтения § 1');
assert.ok(doc.querySelector('#psel-grid .psel-card.final'), 'есть карточка финала');
});
}
test('ch2/ch3: §§ без билдеров — заглушка движка (каркас ждёт наполнения)', async () => {
for (const f of ['math_5_ch2.html', 'math_5_ch3.html']) {
const { doc } = await loadDom(f);
assert.ok(doc.querySelector('#p1-body .m6-placeholder'), f + ': заглушка § 1');
}
});
test('ch1: §1 «как решать задачу», §2 «разрядная таблица», финал-боссы', async () => {
const { doc, errors } = await loadDom('math_5_ch1.html');
const win = doc.defaultView;
assert.ok(!doc.querySelector('#p1-body .m6-placeholder'), '§1 наполнен (не заглушка)');
assert.equal(doc.querySelectorAll('#p1-iv1 [data-step]').length, 4, '§1: 4 кнопки шагов');
assert.ok(doc.querySelector('#p1-iv2 #p1-pa'), '§1: тренажёр-решатель задач');
win.goTo('p2'); await wait(80);
assert.ok(doc.querySelector('#p2-pv-out table'), '§2: разрядная таблица построена');
assert.ok(doc.querySelector('#p2-iv2 #p2-qa'), '§2: тренажёр «цифра в разряде»');
win.goTo('p3'); await wait(80);
assert.equal(doc.querySelectorAll('#p3-iv1 [data-cmp]').length, 3, '§3: знаки сравнения');
win.goTo('p4'); await wait(80);
assert.ok(doc.querySelector('#p4-fig svg'), '§4: рисунок фигуры');
assert.equal(doc.querySelectorAll('#p4-iv1 [data-f]').length, 4, '§4: 4 варианта фигур');
win.goTo('p5'); await wait(80);
assert.ok(doc.querySelector('#p5-fig svg rect'), '§5: линейка');
win.goTo('p6'); await wait(80);
assert.ok(doc.querySelector('#p6-fig svg'), '§6: координатный луч');
win.goTo('p7'); await wait(80);
assert.ok(doc.querySelector('#p7-fig svg'), '§7: округление на луче');
win.goTo('p8'); await wait(80);
assert.ok(doc.querySelector('#p8-iv2 #p8-xa'), '§8: «найди неизвестное»');
win.goTo('p9'); await wait(80);
assert.ok(doc.querySelector('#p9-fig svg circle'), '§9: прямоугольник из точек');
win.goTo('p10'); await wait(80);
assert.ok(doc.querySelector('#p10-fig svg rect'), '§10: квадрат из клеток');
win.goTo('p11'); await wait(80);
assert.ok(doc.querySelector('#p11-fig svg circle'), '§11: точки-группы с остатком');
win.goTo('p12'); await wait(80);
assert.ok(doc.querySelector('#p12-fig'), '§12: делители-чипсы (НОД)');
win.goTo('final'); await wait(80);
assert.ok(doc.querySelector('#fin-go'), 'финал: арена боссов');
win.bumpProgress('final', 100); await wait(20);
assert.ok(win.M6STATE.achievements.has('ch1_done'), 'достижение «Глава 1 пройдена»');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});
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%');
});