diff --git a/backend/src/db/migrations/046_chemistry7_hub.sql b/backend/src/db/migrations/046_chemistry7_hub.sql
new file mode 100644
index 0000000..e921436
--- /dev/null
+++ b/backend/src/db/migrations/046_chemistry7_hub.sql
@@ -0,0 +1,41 @@
+-- Chemistry 7 hub migration.
+-- Creates chemistry-7 as a full hub textbook (4 chapters) in the style of chemistry-8:
+-- chemistry-7 (hub, html_path = chemistry_7_hub.html)
+-- chemistry-7-ch1 (Первоначальные химические понятия, §§1–12) → chemistry_7_ch1.html
+-- chemistry-7-ch2 (Кислород, §§13–17) → chemistry_7_ch2.html
+-- chemistry-7-ch3 (Водород, §§18–22) → chemistry_7_ch3.html
+-- chemistry-7-ch4 (Вода, §§23–26) → chemistry_7_ch4.html
+--
+-- Source: Шиманович И. Е., Красицкий В. А., Сечко О. И., Хвалюк В. Н.,
+-- «Химия 7», Народная асвета, 2023 (2-е изд.). Контент авторский (наш).
+-- Author left empty per project policy.
+
+-- 1. Insert the parent chemistry-7 hub row (does not exist yet in the catalog).
+INSERT OR IGNORE INTO textbooks
+ (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
+VALUES
+ ('chemistry-7', 'chemistry', 7, 'Химия — 7 класс',
+ '',
+ 'Первый курс химии: первоначальные химические понятия (вещество, атом, элемент, молекула, формула, валентность, химическая реакция и уравнение), кислород и оксиды, водород, кислоты и соли, вода и основания, реакция нейтрализации. 4 главы, 26 параграфов, 5 лабораторных опытов, 4 практические работы.',
+ 'chemistry_7_hub.html', 26, 'emerald', 7, 1, NULL);
+
+-- 2. Insert the 4 children (chapters).
+INSERT OR IGNORE INTO textbooks
+ (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
+VALUES
+ ('chemistry-7-ch1', 'chemistry', 7, 'Химия 7 · Первоначальные химические понятия',
+ '',
+ '§§1–12: химия как наука о веществах, чистые вещества и смеси, атомы и химические элементы, относительная атомная масса, молекулы, простые и сложные вещества, химическая формула и относительная молекулярная масса, валентность, физические и химические явления, закон сохранения массы и химические уравнения. Лабораторный опыт 1, практическая работа 1.',
+ 'chemistry_7_ch1.html', 12, 'emerald', 1, 1, 'chemistry-7'),
+ ('chemistry-7-ch2', 'chemistry', 7, 'Химия 7 · Кислород',
+ '',
+ '§§13–17: воздух как смесь газов, кислород как химический элемент и простое вещество, химические свойства кислорода и горение, оксиды, получение кислорода и катализаторы. Лабораторный опыт 2, практическая работа 2.',
+ 'chemistry_7_ch2.html', 5, 'cyan', 2, 1, 'chemistry-7'),
+ ('chemistry-7-ch3', 'chemistry', 7, 'Химия 7 · Водород',
+ '',
+ '§§18–22: водород как химический элемент и простое вещество, химические свойства водорода, понятие о кислотах и индикаторах, взаимодействие кислот с металлами и ряд активности, соли как продукты замещения. Лабораторные опыты 3 и 4, практическая работа 3.',
+ 'chemistry_7_ch3.html', 5, 'violet', 3, 1, 'chemistry-7'),
+ ('chemistry-7-ch4', 'chemistry', 7, 'Химия 7 · Вода',
+ '',
+ '§§23–26: состав, физические и химические свойства воды, основания как сложные вещества и индикаторы, реакция нейтрализации, охрана окружающей среды. Лабораторный опыт 5, практическая работа 4.',
+ 'chemistry_7_ch4.html', 4, 'blue', 4, 1, 'chemistry-7');
diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js
new file mode 100644
index 0000000..5a1e23d
--- /dev/null
+++ b/backend/tests/chemistry7-page.test.js
@@ -0,0 +1,106 @@
+'use strict';
+/*
+ * Phase 0 jsdom-каркас «Химия 7»: проверяем, что хаб и 4 главы реально
+ * выполняются на движке chem8_engine.js без ошибок скриптов, строится
+ * para-selector с нужным числом карточек, активен первый §, заглушки-builder'ы
+ * рисуют para-hero и кнопку прочтения, а финал-боссы хаба решаются.
+ * Содержание § наполняется в фазах 1–4 — здесь проверяется только каркас.
+ */
+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/biochem-core.js': readF('frontend/js/biochem-core.js'),
+ '/js/chem8_svg.js': readF('frontend/js/chem8_svg.js'),
+ '/js/chem7_svg.js': readF('frontend/js/chem7_svg.js'),
+ '/js/chem8_engine.js': readF('frontend/js/chem8_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: 'chemistry_7_ch1.html', cards: 15, first: 'sec-p1' },
+ { file: 'chemistry_7_ch2.html', cards: 8, first: 'sec-p13' },
+ { file: 'chemistry_7_ch3.html', cards: 9, first: 'sec-p18' },
+ { file: 'chemistry_7_ch4.html', cards: 7, first: 'sec-p23' }
+];
+
+for (const ch of CHAPTERS) {
+ test(`${ch.file}: SPA без ошибок, ${ch.cards} карточек, активен ${ch.first}`, 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 === ch.first, 'активен ' + ch.first);
+ const firstId = ch.first.replace('sec-', '');
+ assert.ok(doc.querySelector('#' + firstId + '-body .para-hero'), 'para-hero первого §');
+ assert.ok(doc.querySelector('#' + firstId + '-body .read-wrap'), 'кнопка прочтения первого §');
+ });
+}
+
+test('ch1: переход к §9 и финалу строит заглушку без ошибок', async () => {
+ const { doc, errors } = await loadDom('chemistry_7_ch1.html');
+ doc.defaultView.goTo('p9'); await wait(80);
+ assert.ok(doc.querySelector('#p9-body .para-hero'), 'para-hero §9');
+ doc.defaultView.goTo('final1'); await wait(80);
+ assert.ok(doc.querySelector('#final1-body .para-hero'), 'para-hero финала');
+ assert.equal(doc.querySelector('#final1-body .read-wrap'), null, 'у финала нет кнопки прочтения');
+ assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
+});
+
+/* ── Хаб: каталог глав + финал курса ── */
+function buildHub() {
+ let html = readF('frontend/textbooks/chemistry_7_hub.html');
+ return html
+ .replace(/')
+ .replace(/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Химия 7 · Глава 1
+
Первоначальные химические понятия: вещество, атом, элемент, молекула, формула, валентность, реакция и уравнение
+
+
+
+
+
+
+
+
+
+ С чего начинается химия
+ Химия изучает вещества, их свойства и превращения. В этой главе вы научитесь главному «языку» химии: узнаете об атомах и химических элементах, научитесь записывать состав веществ формулами, определять валентность и составлять уравнения химических реакций.
+
+
Начать § 1
+
+
Прогресс главы
+
+
0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+