import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; const here = path.dirname(fileURLToPath(import.meta.url)); function readControlApp(): string { return fs.readFileSync(path.join(here, 'ControlApp.tsx'), 'utf8'); } function readControlAppCss(): string { return fs.readFileSync(path.join(here, 'ControlApp.module.css'), 'utf8'); } void test('ControlApp: звук молнии (public/molniya.mp3)', () => { const src = readControlApp(); assert.ok(src.includes('molniya.mp3')); assert.ok(src.includes('playLightningEffectSound')); }); void test('ControlApp: звук заморозки (public/zamorozka.mp3)', () => { const src = readControlApp(); assert.ok(src.includes('zamorozka.mp3')); assert.ok(src.includes('playFreezeEffectSound')); }); void test('ControlApp: звук луча света (public/luch_sveta.mp3)', () => { const appSrc = readControlApp(); const sfxSrc = fs.readFileSync(path.join(here, 'sunbeamSfx.ts'), 'utf8'); assert.ok(appSrc.includes('playSunbeamEffectSound')); assert.ok(appSrc.includes('getSunbeamEffectLifeMs')); assert.ok(sfxSrc.includes('luch_sveta.mp3')); assert.ok(sfxSrc.includes('playbackRate')); assert.ok(sfxSrc.includes('SUNBEAM_PLAYBACK_RATE')); }); void test('ControlApp: звук облака яда (public/oblako-yada.mp3)', () => { const appSrc = readControlApp(); const sfxSrc = fs.readFileSync(path.join(here, 'poisonCloudSfx.ts'), 'utf8'); assert.ok(appSrc.includes('getPoisonCloudEffectLifeMs')); assert.ok(appSrc.includes('playPoisonCloudEffectSound')); assert.ok(sfxSrc.includes('oblako-yada.mp3')); assert.ok(sfxSrc.includes('playbackRate')); }); void test('ControlApp: эффекты в пульте, иконки с тултипами и подписью для a11y', () => { const src = readControlApp(); assert.ok(src.includes('ЭФФЕКТЫ')); assert.ok(src.includes('Инструменты')); assert.ok(src.includes('Эффекты поля')); assert.ok(src.includes('Эффекты действий')); assert.ok(src.includes('Луч света')); assert.ok(src.includes('title="Вода"')); assert.ok(src.includes('title="Облако яда"')); assert.ok(src.includes('title="Туман"')); assert.ok(src.includes('ariaLabel="Туман"')); assert.ok(src.includes('iconOnly')); assert.ok(src.includes('title="Очистить эффекты"')); assert.ok(src.includes('ariaLabel="Очистить эффекты"')); assert.ok(src.includes('#e5484d')); const fx = src.indexOf('ЭФФЕКТЫ'); const story = src.indexOf('СЮЖЕТНАЯ ЛИНИЯ'); assert.ok(fx !== -1 && story !== -1 && fx < story, 'Блок эффектов должен быть выше сюжетной линии'); }); void test('ControlApp: сюжетная линия — колонка сверху вниз и фон как у карточек ветвления', () => { const src = readControlApp(); const css = readControlAppCss(); const story = src.indexOf('СЮЖЕТНАЯ ЛИНИЯ'); assert.ok(story !== -1); assert.ok(src.includes('className={styles.storyScroll}')); assert.match(css, /\.storyScroll[\s\S]*?justify-content:\s*flex-start/); assert.match(css, /\.storyScroll[\s\S]*?background:\s*var\(--color-overlay-dark-2\)/); assert.match(css, /\.branchCard[\s\S]*?background:\s*var\(--color-overlay-dark-2\)/); }); void test('ControlApp: слой кисти не использует курсор not-allowed (ластик тоже crosshair)', () => { const src = readControlApp(); const css = readControlAppCss(); assert.ok(!src.includes("tool.tool === 'eraser' ? 'not-allowed'")); assert.ok(src.includes('className={styles.brushLayer}')); assert.match(css, /\.brushLayer[\s\S]*?cursor:\s*crosshair/); }); void test('ControlApp: радиус кисти не в блоке предпросмотра', () => { const src = readControlApp(); const previewLabel = src.indexOf('Предпросмотр экрана'); const radius = src.indexOf('Радиус кисти'); assert.ok(previewLabel !== -1 && radius !== -1); assert.ok( radius < previewLabel, 'Слайдер радиуса должен быть в пульте (файл: выше заголовка предпросмотра)', ); });