// Прогон лендинга в jsdom: проверяем реальное поведение, а не только синтаксис.
const fs = require('fs');
const { JSDOM } = require('jsdom');
const frag = fs.readFileSync('/home/user/landing_single_block.html', 'utf8');
const html = `${frag}`;
let pass = 0, fail = 0;
const ok = (n, c, extra='') => { c ? (pass++, console.log(` ✓ ${n}${extra?' — '+extra:''}`)) : (fail++, console.log(` ✗ ${n}${extra?' — '+extra:''}`)); };
// перехватываем заявки
const sent = [];
const dom = new JSDOM(html, {
runScripts: 'dangerously',
pretendToBeVisual: true,
url: 'https://example.ru/?utm_source=yandex&utm_campaign=tula_nov',
beforeParse(w) {
w.fetch = (url, opt) => { sent.push({ url, body: JSON.parse(opt.body) }); return Promise.resolve({ ok: true }); };
w.alert = m => console.log(' [alert]', m);
const goals = []; w.__goals = goals;
w.ym = (id, act, name) => goals.push(name);
w.HTMLElement.prototype.scrollIntoView = function(){};
}
});
const { window: w } = dom;
const d = w.document;
const $ = s => d.querySelector(s);
const $$ = s => [...d.querySelectorAll(s)];
const click = el => el.dispatchEvent(new w.MouseEvent('click', { bubbles: true }));
const input = el => el.dispatchEvent(new w.Event('input', { bubbles: true }));
console.log('\n=== 1. СТРУКТУРА ===');
ok('Обёртка #nd существует', !!$('#nd'));
ok('Секций на странице', $$('#nd section').length === 10, $$('#nd section').length + ' шт');
ok('Карточек ЖК', $$('#jkGrid .jk').length === 6, $$('#jkGrid .jk').length + ' шт');
ok('Ровно один H1', $$('#nd .h1').length === 1);
ok('Форм на странице', $$('[data-form]').length === 4, $$('[data-form]').length + ' шт');
ok('У всех форм есть formid', $$('[data-form]').every(f => f.dataset.formid));
ok('Каждая форма содержит чекбокс-согласие', $$('[data-form]').every(f => f.querySelector('.agree a')));
ok('Все CTA-ссылки на /soglasiepd', $$('a[href="/soglasiepd"]').length >= 4, $$('a[href="/soglasiepd"]').length + ' ссылок');
console.log('\n=== 2. МАСКА ТЕЛЕФОНА ===');
const tel = $('[data-formid="hero_main"] input[type="tel"]');
const maskCases = [
['9001234567', '+7 (900) 123-45-67', 'без кода'],
['89001234567', '+7 (900) 123-45-67', 'ввод через 8'],
['79001234567', '+7 (900) 123-45-67', 'ввод через 7'],
['+7 900 123 45 67 89', '+7 (900) 123-45-67', 'обрезка лишних цифр'],
['abc900def1234567xyz', '+7 (900) 123-45-67', 'мусорные символы'],
];
for (const [raw, want, label] of maskCases) {
tel.value = raw; input(tel);
ok(`маска: ${label}`, tel.value === want, `"${raw}" → "${tel.value}"`);
}
console.log('\n=== 3. ВАЛИДАЦИЯ (неполный номер не отправляется) ===');
tel.value = '+7 (900) 12'; input(tel);
$('[data-formid="hero_main"]').dispatchEvent(new w.Event('submit', { bubbles: true, cancelable: true }));
ok('Короткий номер отклонён', sent.length === 0);
ok('Поле подсвечено ошибкой', tel.classList.contains('is-err'));
console.log('\n=== 4. ФИЛЬТР КАТАЛОГА ЖК ===');
const vis = () => $$('#jkGrid .jk').filter(c => !c.hidden).length;
const tabBy = f => $$('#tabs .tab').find(t => t.dataset.f === f);
ok('Изначально видны все', vis() === 6, vis() + '/6');
click(tabBy('business'));
ok('Фильтр «Бизнес»', vis() === 2, vis() + ' карточки');
click(tabBy('done'));
ok('Фильтр «Сдан»', vis() === 2, vis() + ' карточки');
click(tabBy('econom'));
ok('Фильтр «Эконом»', vis() === 1, vis() + ' карточка');
ok('Активный таб подсвечен', tabBy('econom').classList.contains('on'));
ok('Прошлый таб снял подсветку', !tabBy('done').classList.contains('on'));
click(tabBy('all'));
ok('Сброс на «Все»', vis() === 6, vis() + '/6');
ok('Плашка «ничего не найдено» скрыта', $('#jkEmpty').hidden);
console.log('\n=== 5. ИПОТЕЧНЫЙ КАЛЬКУЛЯТОР ===');
const num = s => parseInt(($(s).textContent.match(/[\d\s]+/) || [''])[0].replace(/\s/g, ''), 10);
$('#cPrice').value = 5500000; input($('#cPrice'));
$('#cDown').value = 20; input($('#cDown'));
$('#cTerm').value = 20; input($('#cTerm'));
ok('Платёж 6% / 4,4 млн / 20 лет = 31 523 ₽', num('#cPay') === 31523, num('#cPay') + ' ₽');
ok('Сумма кредита 4 400 000 ₽', num('#cLoan') === 4400000, num('#cLoan'));
ok('Первый взнос 1 100 000 ₽', num('#cDownSum') === 1100000, num('#cDownSum'));
ok('Нужный доход посчитан', num('#cIncome') === 70051, num('#cIncome') + ' ₽/мес');
const chipBase = $$('#cChips .chip').find(c => c.dataset.rate === '17.9');
click(chipBase);
ok('Переключение на базовую 17,9% → платёж вырос', num('#cPay') === 67567, num('#cPay') + ' ₽');
ok('Подпись ставки обновилась', $('#cRateV').textContent === '17,9%', $('#cRateV').textContent);
$('#cTerm').value = 1; input($('#cTerm'));
ok('Склонение «1 год»', $('#cTermV').textContent === '1 год', $('#cTermV').textContent);
$('#cTerm').value = 3; input($('#cTerm'));
ok('Склонение «3 года»', $('#cTermV').textContent === '3 года', $('#cTermV').textContent);
$('#cTerm').value = 20; input($('#cTerm'));
ok('Склонение «20 лет»', $('#cTermV').textContent === '20 лет', $('#cTermV').textContent);
// граничный случай: взнос 80%
$('#cDown').value = 80; input($('#cDown'));
ok('Взнос 80% не ломает расчёт', num('#cPay') > 0 && isFinite(num('#cPay')), num('#cPay') + ' ₽');
$('#cDown').value = 20; input($('#cDown'));
console.log('\n=== 6. МОДАЛКА + ПЕРЕДАЧА РАСЧЁТА ===');
click($('[data-modal="mortgage"]'));
ok('Модалка открылась', $('#modal').classList.contains('on'));
ok('Расчёт прокинут в скрытое поле', /Цена .*взнос .*срок .*ставка/.test($('#mCalc').value), '"' + $('#mCalc').value + '"');
click($('#modal [data-close]'));
ok('Модалка закрылась по крестику', !$('#modal').classList.contains('on'));
console.log('\n=== 7. КВИЗ ===');
const stepOn = () => $$('#qz .quiz-step').findIndex(s => s.classList.contains('on'));
ok('Старт с шага 1', stepOn() === 0);
ok('Кнопка «Назад» скрыта на первом шаге', $('#qzBack').hidden);
click($$('#qz [data-single]')[0].querySelectorAll('.opt')[1]); // Инвестиции
ok('Вариант отмечен', $$('#qz [data-single]')[0].querySelectorAll('.opt')[1].classList.contains('on'));
click($('#qzNext'));
ok('Переход на шаг 2', stepOn() === 1, 'шаг ' + (stepOn() + 1));
ok('Прогресс-бар растёт', $('#qzFill').style.width === '40%', $('#qzFill').style.width);
$('#qzArea').value = 75; input($('#qzArea'));
ok('Площадь отображается', $('#qzAreaV').textContent === '75 м²', $('#qzAreaV').textContent);
click($('#qzNext'));
$('#qzBud').value = 7500000; input($('#qzBud'));
ok('Бюджет с разделителями', $('#qzBudV').textContent === '7 500 000 ₽', $('#qzBudV').textContent);
click($('#qzNext'));
ok('Шаг 4 (срок покупки)', stepOn() === 3);
click($('#qzBack'));
ok('Кнопка «Назад» работает', stepOn() === 2, 'вернулись на шаг ' + (stepOn() + 1));
click($('#qzNext'));
click($$('#qz [data-single]')[1].querySelectorAll('.opt')[0]); // Сейчас
click($('#qzNext'));
ok('Дошли до финального шага', stepOn() === 4, 'шаг ' + (stepOn() + 1));
ok('Прогресс 100%', $('#qzFill').style.width === '100%');
console.log('\n=== 8. ОТПРАВКА ЗАЯВКИ ИЗ КВИЗА ===');
const qForm = $('[data-formid="quiz_main"]');
qForm.querySelector('input[name="name"]').value = 'Иван';
const qTel = qForm.querySelector('input[type="tel"]');
qTel.value = '9001234567'; input(qTel);
qForm.dispatchEvent(new w.Event('submit', { bubbles: true, cancelable: true }));
setTimeout(() => {
ok('Заявка ушла (1 запрос)', sent.length === 1, sent.length + ' шт');
const p = sent[0] ? sent[0].body : {};
console.log('\n Payload, который получит CRM:');
Object.entries(p).forEach(([k, v]) => console.log(` ${k}: ${v}`));
ok('Телефон в payload', p.phone === '+7 (900) 123-45-67');
ok('Имя в payload', p.name === 'Иван');
ok('formid = quiz_main', p.formid === 'quiz_main');
ok('Ответы квиза приложены', p.quiz && p.quiz.includes('Инвестиции') && p.quiz.includes('75 м²') && p.quiz.includes('7 500 000'));
ok('UTM-метки сохранены', (p.utm || '').includes('utm_source=yandex'));
ok('Кнопка заблокирована от двойной отправки / показан экран «принято»', /Заявка принята/.test(qForm.innerHTML));
console.log('\n=== 9. ЦЕЛИ МЕТРИКИ ===');
const g = w.__goals;
console.log(' отправлено целей:', JSON.stringify(g));
ok('calc_used', g.includes('calc_used'));
ok('calc_cta', g.includes('calc_cta'));
ok('form_submit', g.includes('form_submit'));
ok('quiz_complete', g.includes('quiz_complete'));
console.log('\n=== 10. FAQ-АККОРДЕОН ===');
const items = $$('#faqList .faq-i');
click(items[0].querySelector('.faq-q'));
ok('Первый вопрос раскрылся', items[0].classList.contains('open'));
click(items[2].querySelector('.faq-q'));
ok('Открылся третий', items[2].classList.contains('open'));
ok('Первый закрылся (режим «аккордеон»)', !items[0].classList.contains('open'));
click(items[2].querySelector('.faq-q'));
ok('Повторный клик закрывает', !items[2].classList.contains('open'));
console.log('\n=== 11. КОНТАКТЫ ИЗ CONFIG ===');
ok('tel: подставлен во все ссылки', $$('a[href^="tel:"]').every(a => a.href === 'tel:+74872000000'), $$('a[href^="tel:"]').length + ' ссылок');
ok('WhatsApp со текстом-префиллом', $$('a[href*="wa.me"]').every(a => a.href.includes('wa.me/79000000000') && a.href.includes('text=')));
ok('Перелинковка на страницы ЖК в футере', $$('.foot a[href^="/"]').length >= 6, $$('.foot a[href^="/"]').length + ' ссылок');
console.log(`\n${'='.repeat(46)}`);
console.log(`ИТОГО: ${pass} пройдено, ${fail} провалено`);
console.log('='.repeat(46));
}, 120);