Files
Learn_System/backend/tests/math5-page.test.js
Maxim Dolgolyov 5a2a1be089 feat(math5): Глава 3 «Обыкновенные дроби» — §1–18 + финал (Sonnet по эталону)
Дроби и доли, основное свойство и сокращение, смешанные числа, сравнение,
сложение/вычитание/умножение/деление дробей, задачи на дроби; геометрия:
параллельные/перпендикулярные прямые, периметр многоугольника, площадь и
площадь треугольника, среднее арифметическое, столбчатые диаграммы,
параллелепипед и объём (2D-изометрия). Inline-SVG визуалы (полоса долей,
сетка умножения, изометрия). Реализовано Sonnet-агентом инкрементально по
образцу math_5_ch1; проверено: грузится без ошибок, §1–18 без заглушек.

Учебник «Математика 5» наполнен ЦЕЛИКОМ (3 главы, 44 §). Тесты math5: 12/12.

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

205 lines
12 KiB
JavaScript
Raw Permalink 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('ch3: дроби + геометрия + объём + финал', async () => {
const { doc, errors } = await loadDom('math_5_ch3.html');
const win = doc.defaultView;
assert.ok(!doc.querySelector('#p1-body .m6-placeholder'), '§1 наполнен');
assert.ok(doc.querySelector('#p1-fig svg'), '§1: полоса долей (SVG)');
win.goTo('p7'); await wait(80);
assert.ok(doc.querySelector('#p7-fig svg'), '§7: сетка умножения дробей (SVG)');
win.goTo('p17'); await wait(80);
assert.ok(doc.querySelector('#p17-fig svg'), '§17: параллелепипед (изометрия SVG)');
win.goTo('p18'); await wait(80);
assert.ok(doc.querySelector('#p18-fig svg'), '§18: объём кубиками (изометрия SVG)');
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('ch3_done'), 'достижение «Глава 3 пройдена»');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});
test('ch3: все § §1–§18 наполнены (нет заглушек)', async () => {
const { doc } = await loadDom('math_5_ch3.html');
const win = doc.defaultView;
for (let n = 1; n <= 18; n++) {
win.goTo('p' + n); await wait(20);
assert.ok(!doc.querySelector('#p' + n + '-body .m6-placeholder'), '§' + n + ' наполнен');
}
});
test('ch2: выражения/уравнения/углы + финал', async () => {
const { doc, errors } = await loadDom('math_5_ch2.html');
const win = doc.defaultView;
assert.ok(!doc.querySelector('#p1-body .m6-placeholder'), '§1 наполнен');
assert.ok(doc.querySelector('#p1-iv1 #p1-a'), '§1: тренажёр выражений');
win.goTo('p3'); await wait(80);
assert.ok(doc.querySelector('#p3-iv1 #p3-a'), '§3: решение уравнения');
win.goTo('p6'); await wait(80);
assert.ok(doc.querySelector('#p6-fig svg'), '§6: SVG угла');
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('ch2_done'), 'достижение «Глава 2 пройдена»');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});
test('ch2: все § §1–§9 наполнены (нет заглушек)', async () => {
const { doc } = await loadDom('math_5_ch2.html');
const win = doc.defaultView;
for (let n = 1; n <= 9; n++) {
win.goTo('p' + n); await wait(20);
assert.ok(!doc.querySelector('#p' + n + '-body .m6-placeholder'), '§' + n + ' наполнен');
}
});
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('p13'); await wait(80);
assert.ok(doc.querySelector('#p13-out'), '§13: чекер делимости');
win.goTo('p14'); await wait(80);
assert.equal(doc.querySelectorAll('#p14-grid [data-n]').length, 29, '§14: решето 2..30');
win.goTo('p15'); await wait(80);
assert.ok(doc.querySelector('#p15-iv1 #p15-a'), '§15: задачи из жизни');
win.goTo('p16'); await wait(80);
assert.ok(doc.querySelector('#p16-iv1 #p16-a'), '§16: задачи на движение');
win.goTo('p17'); await wait(80);
assert.ok(doc.querySelector('#p17-q') && doc.querySelectorAll('#p17-hopt [data-oi]').length === 3, '§17: римские цифры + квиз');
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('ch1: все § §1–§17 наполнены (нет заглушек движка)', async () => {
const { doc } = await loadDom('math_5_ch1.html');
const win = doc.defaultView;
for (let n = 1; n <= 17; n++) {
win.goTo('p' + n); await wait(20);
assert.ok(!doc.querySelector('#p' + n + '-body .m6-placeholder'), '§' + n + ' наполнен (не заглушка)');
}
});
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%');
});