'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(/') .replace(/'); }); 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%'); });