'use strict'; /* * Phase 0 jsdom-каркас «Математика 6»: хаб и 6 глав выполняются на движке * math6_engine.js без ошибок скриптов; para-selector строится с нужным числом * карточек; активен первый §; движок рендерит секции и заглушку/контент с кнопкой * прочтения. Содержание §§ наполняется по главам — здесь проверяется каркас. */ 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_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_6_ch1.html', cards: 12 }, { file: 'math_6_ch2.html', cards: 9 }, { file: 'math_6_ch3.html', cards: 5 }, { file: 'math_6_ch4.html', cards: 11 }, { file: 'math_6_ch5.html', cards: 5 }, { file: 'math_6_ch6.html', cards: 6 } ]; 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('hub: 6 карточек глав', async () => { const { doc, errors } = await loadDom('math_6_hub.html'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 6, '6 глав'); }); test('ch1 Волна 1: интерактивы §1–§3 монтируются без ошибок', async () => { const { doc, errors } = await loadDom('math_6_ch1.html'); const win = doc.defaultView; // §1 строится при загрузке assert.ok(doc.querySelector('#p1-iv1 #p1-c'), 'разрядный конструктор §1'); assert.ok(doc.querySelector('#p1-iv2 #p1-qq'), 'разряд-квиз §1'); win.goTo('p2'); await wait(80); assert.ok(doc.querySelector('#p2-cfig svg'), 'числовая прямая сравнения §2'); assert.ok(doc.querySelector('#p2-iv2 #p2-rq'), 'тренажёр округления §2'); win.goTo('p3'); await wait(80); assert.ok(doc.querySelector('#p3-afig svg'), 'координатный луч §3'); assert.ok(doc.querySelectorAll('#p3-iv2 [data-pt]').length === 4, 'выбор точки A–D §3'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); test('навигация и прогресс: переход на § и отметка прочтения', async () => { const { doc, errors } = await loadDom('math_6_ch1.html'); const win = doc.defaultView; win.goTo('p4'); await wait(60); assert.ok(doc.querySelector('#sec-p4.active'), 'перешли на § 4'); /* отметка прочтения начисляет XP */ const btn = doc.querySelector('#p4-body [data-read]'); assert.ok(btn, 'кнопка прочтения § 4'); btn.click(); await wait(20); assert.ok((win.M6STATE.progress.p4 || 0) >= 30, 'прогресс § 4 вырос после прочтения'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); });