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>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
-- Math 5 hub migration.
|
||||
-- Creates math-5 as a full hub textbook (3 chapters) in the style of math-6:
|
||||
-- math-5 (hub, html_path = math_5_hub.html)
|
||||
-- math-5-ch1 (Натуральные числа, §§1–17) → math_5_ch1.html
|
||||
-- math-5-ch2 (Выражения. Уравнения, §§1–9) → math_5_ch2.html
|
||||
-- math-5-ch3 (Обыкновенные дроби, §§1–18) → math_5_ch3.html
|
||||
--
|
||||
-- Source: Герасимов В. Д., Пирютко О. Н., Лобанов А. П., «Математика. 5 класс»,
|
||||
-- в 2 частях, Минск: Адукацыя і выхаванне, 2020 (2-е изд.). Контент авторский (наш).
|
||||
-- Author left empty per project policy.
|
||||
|
||||
-- 1. Parent hub row.
|
||||
INSERT OR IGNORE INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
|
||||
VALUES
|
||||
('math-5', 'math', 5, 'Математика — 5 класс',
|
||||
'',
|
||||
'Полный курс математики 5 класса: натуральные числа и действия с ними, делимость, степень, выражения и уравнения, углы, обыкновенные дроби и смешанные числа, периметр, площадь и объём. 3 главы, 44 параграфа, интерактивные тренажёры, визуализации и финалы-боссы.',
|
||||
'math_5_hub.html', 47, 'indigo', 5, 1, NULL);
|
||||
|
||||
-- 2. Three chapters.
|
||||
INSERT OR IGNORE INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
|
||||
VALUES
|
||||
('math-5-ch1', 'math', 5, 'Математика 5 · Натуральные числа',
|
||||
'',
|
||||
'§§1–17: как решать задачу, чтение и запись чисел и разряды, сравнение, точка/прямая/луч/отрезок, измерение отрезков, координатный луч, округление, сложение/вычитание/умножение/деление, степень, деление с остатком, делители, НОД и НОК, признаки делимости, простые и составные числа, разложение на множители.',
|
||||
'math_5_ch1.html', 18, 'indigo', 1, 1, 'math-5'),
|
||||
('math-5-ch2', 'math', 5, 'Математика 5 · Выражения. Уравнения',
|
||||
'',
|
||||
'§§1–9: числовые выражения и порядок действий, выражения с переменными, уравнение и его корень, формулы, решение задач с помощью уравнений, угол — измерение и построение углов транспортиром.',
|
||||
'math_5_ch2.html', 10, 'teal', 2, 1, 'math-5'),
|
||||
('math-5-ch3', 'math', 5, 'Математика 5 · Обыкновенные дроби',
|
||||
'',
|
||||
'§§1–18: обыкновенные дроби и доли, основное свойство дроби, правильные/неправильные и смешанные числа, сравнение, сложение и вычитание, умножение и деление дробей, задачи на дроби, параллельные и перпендикулярные прямые, ломаная и многоугольник, периметр, площадь и площадь треугольника, среднее арифметическое, диаграммы, прямоугольный параллелепипед и объём.',
|
||||
'math_5_ch3.html', 19, 'rose', 3, 1, 'math-5');
|
||||
@@ -0,0 +1,97 @@
|
||||
'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%');
|
||||
});
|
||||
Reference in New Issue
Block a user