diff --git a/README.md b/README.md index 489e085..7ab059e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#build--distribution) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) [![Downloads](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=downloads)](https://github.com/CaYatur/SoundVisualizer/releases) -[![Tests](https://img.shields.io/badge/tests-2543%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-2547%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1341,7 +1341,7 @@ npm test npm start -- --smoke ``` -**2543 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2547 unit tests, all passing.** They are written to check answers, not to exercise lines: - **Formulas** are checked against values derived by hand from their definitions — Viviani's curve staying on its sphere, the torus tube radius, Chladni's m↔n antisymmetry, every attractor diff --git a/README.tr.md b/README.tr.md index 890fb7f..2f6297d 100644 --- a/README.tr.md +++ b/README.tr.md @@ -12,7 +12,7 @@ [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#paketleme--dağıtım) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) [![İndirme](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=indirme)](https://github.com/CaYatur/SoundVisualizer/releases) -[![Test](https://img.shields.io/badge/test-2543%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-2547%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1341,7 +1341,7 @@ npm test npm start -- --smoke ``` -**2543 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: +**2547 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: - **Formüller**, tanımlarından elle türetilmiş değerlerle sınanıyor — Viviani eğrisinin küre üzerinde kalması, simidin boru yarıçapı, Chladni'nin m↔n antisimetrisi, her çekicinin sınırlı diff --git a/ROADMAP.md b/ROADMAP.md index 25ebfa0..aebef4e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ npm test npm start -- --smoke ``` -- **2543 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2547 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; 105 came with v3.1.1; 163 came with v3.1.2; 157 came with v3.1.3 — 1128 at that tag — 469 more with v3.1.4, most of them from the MilkDrop work, and 878 on `main` since. diff --git a/src/admin/admin.css b/src/admin/admin.css index b056eb7..c9a11fb 100644 --- a/src/admin/admin.css +++ b/src/admin/admin.css @@ -828,11 +828,6 @@ select:focus { border-color: var(--accent); } } .footer-end { display: flex; align-items: center; gap: 12px; margin-left: auto; min-width: 0; } .footer .hint { font-size: 11px; line-height: 1.35; color: var(--muted2); text-align: right; } -#updatesBtn.has-update { - color: var(--ok); - border-color: var(--ok); - background: rgba(67, 217, 138, .14); -} kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px; font-size: 10.5px; } /* scrollbar */ @@ -842,32 +837,6 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p ::-webkit-scrollbar-track { background: transparent; } /* Settings is the left-rail Ayarlar category; Updates is a footer modal. */ -.upd-backdrop { - position: fixed; inset: 0; z-index: 410; display: grid; place-items: center; - background: rgba(3, 3, 8, .62); backdrop-filter: blur(3px); - animation: askIn .12s ease-out; - padding: 20px; -} -.upd-backdrop.hidden { display: none; } -.upd-modal { - width: min(520px, calc(100vw - 40px)); max-height: min(820px, calc(100vh - 40px)); - overflow: auto; padding: 18px 18px 16px; - border: 1px solid var(--line2); border-radius: 14px; - background: var(--card); box-shadow: 0 28px 80px rgba(0, 0, 0, .6); -} -.upd-modal-head { - display: flex; align-items: center; justify-content: space-between; gap: 12px; - margin-bottom: 14px; -} -.upd-modal-head h2 { font-size: 16px; font-weight: 700; } -.upd-modal-close { - width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--line); - background: transparent; color: var(--muted); font-size: 18px; line-height: 1; - cursor: pointer; -} -.upd-modal-close:hover { background: var(--card2); color: var(--text); } -.upd-modal-body .ctrl { margin-bottom: 12px; } -.upd-modal-body .upd-panel { margin-top: 4px; } /* ================= UYGULAMA İÇİ ONAY + BİLDİRİM ================= Sistem confirm/alert pencereleri renderer'ı kilitliyor ve çok ekranlı diff --git a/src/admin/admin.js b/src/admin/admin.js index 02a94bd..fa5ea4c 100644 --- a/src/admin/admin.js +++ b/src/admin/admin.js @@ -2412,7 +2412,23 @@ }, { id: 'templates', - roots: ['visualizer', 'background', 'postfx'], + /* Badge/reset must cover every SCENE_KEYS field apply() touches. + Partial roots left modulation/layers/geometry dirty and — with + layerStack on — syncStackState re-cleared classic visualizer to + 'none' after a partial reset, so the badge bounced back alone. */ + roots: (window.SVTemplates && window.SVTemplates.SCENE_KEYS + ? window.SVTemplates.SCENE_KEYS.slice() + : ['background', 'visualizer', 'geometry', 'postfx', 'layers', 'layerStack', 'logo', + 'modulation', 'transition', 'custom', 'milkdrop', 'images', 'feedback']), + /* Same preserves as templates.apply / resetScene — user setup, not scene. */ + rootOmit: [ + 'background.transparent', 'background.transparentKey', 'background.coverTaskbar', + 'logo.src', 'logo.libraryId', 'logo.kind', 'logo.source', 'logo.enabled', + ], + /* TEMP: hide card modified badge + circular reset on Ready Templates. + Applying a template still changes SCENE_KEYS; re-enable by setting + hideCardReset: false once resetScene bounce is fully validated UX-wise. */ + hideCardReset: true, category: 'library', icon: 'sparkles', wide: true, @@ -2895,7 +2911,7 @@ }, { /* Uygulama: Dil / Pencere / Panel. Güncellemeler ayrı tam genişlik - kartta (settings-updates) ve footer indirme düğmesi modalında. */ + kartta (settings-updates). Footer indirme modalı kaldırıldı. */ id: 'settings-main', category: 'settings', wide: true, @@ -3077,6 +3093,11 @@ return out; } + function sectionShowsResetChrome(sec) { + /* hideCardReset: TEMP gate for Ready Templates card chrome. */ + return !(sec && sec.hideCardReset); + } + function countModified(paths) { return paths.filter(isModified).length; } @@ -3086,6 +3107,7 @@ sectionSchema().forEach((sec) => { if (sec.category !== catId) return; if (sec.show && !sec.show()) return; + if (!sectionShowsResetChrome(sec)) return; n += countModified(sectionPaths(sec)); }); return n; @@ -3169,7 +3191,7 @@ root.classList.toggle('single', sections.length === 1 || sections.every((s) => s.wide)); // Sıfırlanacak bir şey yoksa (ör. Kitaplık) düğme boşuna durmasın - const resettable = sections.some((s) => countModified(sectionPaths(s)) > 0); + const resettable = sections.some((s) => sectionShowsResetChrome(s) && countModified(sectionPaths(s)) > 0); $('catResetBtn').classList.toggle('hidden', !resettable); sections.forEach((sec) => { @@ -3245,7 +3267,7 @@ const tail = visible.filter((d) => d.tail); const showAdvanced = advancedOn; - const modCount = countModified(sectionPaths(sec)); + const modCount = sectionShowsResetChrome(sec) ? countModified(sectionPaths(sec)) : 0; const head = el('div', { class: 'card-head' }, [ el('span', { class: 'ico', icon: sec.icon }), el('div', { class: 'ch-main' }, [ @@ -3369,7 +3391,7 @@ sections.forEach((sec, i) => { const card = cards[i]; if (!card) return; - const n = countModified(sectionPaths(sec)); + const n = sectionShowsResetChrome(sec) ? countModified(sectionPaths(sec)) : 0; let chip = card.querySelector('.chip-mod'); const acts = card.querySelector('.ch-actions'); if (n > 0 && !chip && acts) { @@ -3393,7 +3415,7 @@ }); renderNav(); - const resettable = sections.some((s) => countModified(sectionPaths(s)) > 0); + const resettable = sections.some((s) => sectionShowsResetChrome(s) && countModified(sectionPaths(s)) > 0); const catBtn = $('catResetBtn'); if (catBtn) catBtn.classList.toggle('hidden', !resettable); } @@ -3407,15 +3429,29 @@ } async function resetSection(sec) { + if (sec && sec.hideCardReset) return; const paths = sectionPaths(sec); if (!paths.length) return; const ok = await svConfirm('Bu bölümdeki ayarlar varsayılana dönecek.', { danger: true, okText: 'Bölümü sıfırla' }); if (!ok) return; - const defaults = window.SV.defaultConfig(); - paths.forEach((p) => { - const dv = getPath(defaults, p); - if (dv !== undefined) setPath(cfg, p, window.SV.clone(dv)); - }); + /* Ready Templates: whole-object SCENE_KEYS restore (not leaf setPath). + Leaf reset missed array/object identity and left layerStack on, so + the next syncStackState re-dirtied visualizer.type back to 'none'. */ + if (sec.id === 'templates' && window.SVTemplates && typeof window.SVTemplates.resetScene === 'function') { + window.SVTemplates.resetScene(cfg, { + defaultConfig: window.SV.defaultConfig, + clone: window.SV.clone, + }); + if (window.SVTemplatePanel && typeof window.SVTemplatePanel.clearLastApplied === 'function') { + window.SVTemplatePanel.clearLastApplied(); + } + } else { + const defaults = window.SV.defaultConfig(); + paths.forEach((p) => { + const dv = getPath(defaults, p); + if (dv !== undefined) setPath(cfg, p, window.SV.clone(dv)); + }); + } push(true); render(); } @@ -3593,6 +3629,7 @@ sectionSchema() .filter((s) => s.category === catId && (!s.show || s.show())) .forEach((sec) => { + if (!sectionShowsResetChrome(sec)) return; sectionPaths(sec).forEach((p) => { const dv = getPath(defaults, p); if (dv !== undefined) setPath(cfg, p, window.SV.clone(dv)); diff --git a/src/admin/index.html b/src/admin/index.html index 3bcb394..ea18483 100644 --- a/src/admin/index.html +++ b/src/admin/index.html @@ -205,22 +205,11 @@

Sahneler

- - - + diff --git a/src/admin/template-panel.js b/src/admin/template-panel.js index 6689726..2901c8b 100644 --- a/src/admin/template-panel.js +++ b/src/admin/template-panel.js @@ -153,5 +153,8 @@ P().toast('"' + t.name + '" uygulandı.'); } - window.SVTemplatePanel = { panel }; + window.SVTemplatePanel = { + panel, + clearLastApplied() { lastApplied = ''; }, + }; })(); diff --git a/src/admin/updates-panel.js b/src/admin/updates-panel.js index dbb5c3a..291c3f4 100644 --- a/src/admin/updates-panel.js +++ b/src/admin/updates-panel.js @@ -1,10 +1,8 @@ 'use strict'; /* Güncellemeler (#640). - Ana süreç denetler (src/main/updater.js) ve durumu gönderir. Aynı içerik - Settings’teki tam genişlik Güncellemeler kartında ve footer indirme - düğmesinden açılan modalda kullanılır (panel() ortak kaynak). - Sürüm notları yalnız METİN; adresler ana süreçte açılır. + Ana süreç denetler (src/main/updater.js) ve durumu gönderir. İçerik Settings’teki tam genişlik Güncellemeler kartında gösterilir (panel()). + Sürüm notları yalnız METİN; adresler ana süreçte açılır. Footer indirme modalı kaldırıldı. Yeni sürüm ilk görüldüğünde (oturum başına sürüm başına bir kez) toast çıkar; modal kapalı olsa da kullanıcı haberdar olur. */ @@ -33,7 +31,8 @@ dev: 'Geliştirme kopyası: yalnız denetim yapılır; otomatik denetim kapalı.', }; - function fmtDate(iso) { + +function fmtDate(iso) { const d = new Date(iso); if (isNaN(d.getTime())) return ''; try { @@ -43,7 +42,7 @@ } } - function statusText(s) { +function statusText(s) { if (!s || s.status === 'idle') return s && s.auto ? 'Henüz denetlenmedi.' : 'Otomatik denetim bu kopyada kapalı; elle denetleyebilirsiniz.'; if (s.status === 'checking') return 'Denetleniyor…'; if (s.status === 'latest') return 'Güncel: en yeni sürüm kurulu.'; @@ -55,18 +54,17 @@ return ''; } - function onStatus(s) { +function onStatus(s) { state = s; if (s && s.status === 'available' && !s.skipped && !toasted.has(s.latest) && P() && P().toast) { toasted.add(s.latest); P().toast(tt('Yeni sürüm:') + ' v' + s.latest + ' — ' + tt('Güncellemeler'), 'ok'); } refresh(); - syncFooterBtn(); } - function refresh() { - /* Kart ve modal aynı anda .upd-panel tutabilir; hepsini yenile. */ +function refresh() { + /* Settings kartında birden fazla .upd-panel tutabilir; hepsini yenile. */ if (typeof document !== 'undefined') { const nodes = Array.from(document.querySelectorAll('.upd-panel')); if (nodes.length) { @@ -81,70 +79,7 @@ box.replaceWith(buildPanel()); } - function syncFooterBtn() { - if (typeof document === 'undefined') return; - const btn = document.getElementById('updatesBtn'); - if (!btn) return; - const hot = !!(state && state.status === 'available' && !state.skipped); - btn.classList.toggle('has-update', hot); - } - - function modeSelect() { - const p = P(); - if (!p || !p.el || !p.cfg) return null; - const el = p.el; - const cfg = p.cfg(); - if (!cfg.updates) cfg.updates = { mode: 'notify' }; - const sel = el('select', { - onchange: (e) => { - cfg.updates.mode = e.target.value; - if (p.push) p.push(true); - }, - }); - [ - ['notify', 'Açık — yeni sürümü haber ver'], - ['auto', 'Otomatik — indir ve kapanırken kur'], - ['off', 'Kapalı'], - ].forEach(([v, t]) => { - const o = el('option', { value: v, text: tt(t) }); - if (String(cfg.updates.mode || 'notify') === v) o.selected = true; - sel.appendChild(o); - }); - return p.row(tt('Güncellemeleri Denetle'), sel); - } - - function fillModalBody() { - if (typeof document === 'undefined') return; - const body = document.getElementById('updatesModalBody'); - if (!body || !P() || !P().el) return; - body.textContent = ''; - const mode = modeSelect(); - if (mode) body.appendChild(mode); - body.appendChild(panel()); - } - - function openModal() { - if (typeof document === 'undefined') return; - const backdrop = document.getElementById('updatesBackdrop'); - if (!backdrop) return; - fillModalBody(); - backdrop.classList.remove('hidden'); - backdrop.setAttribute('aria-hidden', 'false'); - const closeBtn = document.getElementById('updatesClose'); - if (closeBtn) closeBtn.focus(); - } - - function closeModal() { - if (typeof document === 'undefined') return; - const backdrop = document.getElementById('updatesBackdrop'); - if (!backdrop) return; - backdrop.classList.add('hidden'); - backdrop.setAttribute('aria-hidden', 'true'); - const btn = document.getElementById('updatesBtn'); - if (btn) btn.focus(); - } - - function buildPanel() { +function buildPanel() { const p = P(); const el = p.el; const s = state || { status: 'idle' }; @@ -214,39 +149,17 @@ return root; } - function panel() { +function panel() { return buildPanel(); } - function wireModalChrome() { - if (typeof document === 'undefined') return; - const btn = document.getElementById('updatesBtn'); - const backdrop = document.getElementById('updatesBackdrop'); - const closeBtn = document.getElementById('updatesClose'); - if (btn) btn.addEventListener('click', openModal); - if (closeBtn) closeBtn.addEventListener('click', closeModal); - if (backdrop) { - backdrop.addEventListener('click', (e) => { - if (e.target === backdrop) closeModal(); - }); - } - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && backdrop && !backdrop.classList.contains('hidden')) { - e.preventDefault(); - closeModal(); - } - }); - } - - function init() { - wireModalChrome(); - syncFooterBtn(); +function init() { if (!window.api || !window.api.onUpdatesStatus) return; window.api.onUpdatesStatus(onStatus); if (window.api.updatesState) window.api.updatesState().then(onStatus).catch(() => {}); } - window.SVUpdatesPanel = { panel, init, openModal, closeModal, _state: () => state, _onStatus: onStatus }; + window.SVUpdatesPanel = { panel, init, _state: () => state, _onStatus: onStatus }; if (typeof document !== 'undefined') { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); diff --git a/src/shared/templates.js b/src/shared/templates.js index f6d9e55..341d38c 100644 --- a/src/shared/templates.js +++ b/src/shared/templates.js @@ -608,6 +608,42 @@ return null; } + /* Factory reset for Ready Templates card. + + Mirrors apply()'s SCENE_KEYS wipe + user-setup preserves, without + merging a template patch. Keeps audio/display/stream/lighting intact + and restores logo identity + transparent-window flags the same way + apply() does, so the card badge can clear permanently. */ + function resetScene(cfg, env) { + if (!cfg || !env) return cfg; + const { defaultConfig, clone } = env; + const def = defaultConfig(); + const keepTransparent = !!(cfg.background && cfg.background.transparent); + const keepKey = cfg.background && cfg.background.transparentKey; + const keepCover = cfg.background && cfg.background.coverTaskbar; + const logoSrc = (cfg.logo && cfg.logo.src) || null; + const logoLibraryId = (cfg.logo && cfg.logo.libraryId) || null; + const logoKind = (cfg.logo && cfg.logo.kind) || null; + const logoSource = (cfg.logo && cfg.logo.source) || null; + const logoEnabled = cfg.logo && typeof cfg.logo.enabled === 'boolean' ? cfg.logo.enabled : null; + for (const k of SCENE_KEYS) { + if (def[k] !== undefined) cfg[k] = clone(def[k]); + } + if (cfg.logo) { + if (logoSrc) cfg.logo.src = logoSrc; + if (logoLibraryId) cfg.logo.libraryId = logoLibraryId; + if (logoKind) cfg.logo.kind = logoKind; + if (logoSource) cfg.logo.source = logoSource; + if (logoEnabled !== null) cfg.logo.enabled = logoEnabled; + } + if (cfg.background) { + cfg.background.transparent = keepTransparent; + if (keepKey != null) cfg.background.transparentKey = keepKey; + if (keepCover != null) cfg.background.coverTaskbar = !!keepCover; + } + return cfg; + } + function apply(cfg, tpl, env) { if (!cfg || !tpl || !env) return cfg; const { defaultConfig, deepMerge, clone } = env; @@ -712,7 +748,7 @@ return out; } - const api = { TEMPLATES, apply, groups }; + const api = { TEMPLATES, SCENE_KEYS, apply, resetScene, groups }; if (typeof module !== 'undefined' && module.exports) module.exports = api; if (typeof window !== 'undefined') window.SVTemplates = api; })(); diff --git a/tests/admin-templates-reset.test.js b/tests/admin-templates-reset.test.js new file mode 100644 index 0000000..1a15202 --- /dev/null +++ b/tests/admin-templates-reset.test.js @@ -0,0 +1,31 @@ +'use strict'; +/* Ready Templates: card modified badge + reset are TEMP hidden (hideCardReset). + * SCENE_KEYS alignment + resetScene remain so chrome can be re-enabled later. */ +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); + +const admin = fs.readFileSync(path.join(__dirname, '..', 'src', 'admin', 'admin.js'), 'utf8'); +const templates = fs.readFileSync(path.join(__dirname, '..', 'src', 'shared', 'templates.js'), 'utf8'); +const panel = fs.readFileSync(path.join(__dirname, '..', 'src', 'admin', 'template-panel.js'), 'utf8'); + +test('templates section hides card reset chrome via hideCardReset flag', () => { + const start = admin.indexOf("id: 'templates'"); + assert.ok(start > 0); + const slice = admin.slice(start, start + 1400); + assert.match(slice, /hideCardReset:\s*true/); + assert.match(admin, /function sectionShowsResetChrome/); + assert.match(admin, /sectionShowsResetChrome\(sec\)/); +}); + +test('templates roots still follow SCENE_KEYS for easy re-enable', () => { + const start = admin.indexOf("id: 'templates'"); + const slice = admin.slice(start, start + 1400); + assert.match(slice, /SVTemplates\.SCENE_KEYS/); + assert.match(slice, /rootOmit:/); + assert.match(templates, /function resetScene/); + assert.match(templates, /SCENE_KEYS, apply, resetScene/); + assert.match(admin, /SVTemplates\.resetScene/); + assert.match(panel, /clearLastApplied/); +}); diff --git a/tests/settings-updates-library-scroll.test.js b/tests/settings-updates-library-scroll.test.js index eaab6db..b9939b2 100644 --- a/tests/settings-updates-library-scroll.test.js +++ b/tests/settings-updates-library-scroll.test.js @@ -1,6 +1,6 @@ 'use strict'; /* Settings: Uygulama card (Dil/Pencere/Panel) + separate full-width Updates card. - * Footer download icon opens the same Updates UI as a modal. */ + * Updates live only on the Settings full-width card (no footer modal). */ const test = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); @@ -34,17 +34,18 @@ test('Settings: separate full-width Updates card', () => { assert.doesNotMatch(admin, /id:\s*'updates',\s*\n\s*category:\s*'library'/); }); -test('Updates modal from footer download button', () => { - assert.match(html, /id="updatesBtn"/); - assert.match(html, /id="updatesBackdrop"/); - assert.match(html, /id="updatesModalBody"/); - assert.match(updatesPanel, /function openModal/); - assert.match(updatesPanel, /function closeModal/); - assert.match(updatesPanel, /fillModalBody/); +test('Updates footer modal removed; Settings card panel remains', () => { + assert.doesNotMatch(html, /id="updatesBtn"/); + assert.doesNotMatch(html, /id="updatesBackdrop"/); + assert.doesNotMatch(html, /id="updatesModalBody"/); + assert.doesNotMatch(updatesPanel, /function openModal/); + assert.doesNotMatch(updatesPanel, /function closeModal/); + assert.doesNotMatch(updatesPanel, /fillModalBody/); + assert.doesNotMatch(updatesPanel, /updatesBtn/); assert.match(updatesPanel, /function buildPanel/); - assert.match(css, /\.upd-backdrop/); - assert.match(css, /\.upd-modal/); - assert.doesNotMatch(updatesPanel, /Ayarlar › Güncellemeler|Kitaplık › Güncellemeler/); + assert.match(updatesPanel, /function panel/); + assert.doesNotMatch(css, /\.upd-backdrop/); + assert.doesNotMatch(css, /#updatesBtn/); }); test('Settings groups have distinct block styles with theme tokens', () => { diff --git a/tests/templates.test.js b/tests/templates.test.js index 6910359..d48b5bc 100644 --- a/tests/templates.test.js +++ b/tests/templates.test.js @@ -195,3 +195,42 @@ test('her şablon uygulandığında tam ve geçerli bir yapılandırma verir', ( assert.strictEqual(outCustom.layerStack.enabled, false, 'şablondaki açık layerStack tercihi uygulanmalı'); }); + + +test('resetScene fabrika sahnesine döner ve kurulum/korunan alanlara dokunmaz', () => { + const cfg = SV.defaultConfig(); + cfg.audio.sensitivity = 2.5; + cfg.background.transparent = true; + cfg.background.coverTaskbar = true; + cfg.logo.enabled = true; + cfg.logo.src = 'data:image/png;base64,abc'; + cfg.layerStack.enabled = true; + + const tpl = T.TEMPLATES.find((t) => t.id === 'club-strobe'); + const out = T.apply(cfg, tpl, env); + assert.notStrictEqual(out.visualizer.type, SV.defaultConfig().visualizer.type); + assert.ok((out.postfx || []).length > 0 || (out.modulation.routes || []).length > 0); + + T.resetScene(out, env); + + const def = SV.defaultConfig(); + assert.strictEqual(out.visualizer.type, def.visualizer.type); + assert.deepStrictEqual(out.postfx, def.postfx); + assert.deepStrictEqual(out.modulation.routes, def.modulation.routes); + assert.deepStrictEqual(out.layers, def.layers); + assert.strictEqual(out.layerStack.enabled, def.layerStack.enabled); + assert.strictEqual(out.audio.sensitivity, 2.5, 'kurulum bozulmamalı'); + assert.strictEqual(out.background.transparent, true, 'transparent korunmalı'); + assert.strictEqual(out.background.coverTaskbar, true, 'coverTaskbar korunmalı'); + assert.strictEqual(out.logo.enabled, true, 'logo.enabled korunmalı'); + assert.strictEqual(out.logo.src, 'data:image/png;base64,abc', 'logo.src korunmalı'); +}); + +test('SCENE_KEYS dışa aktarılır ve apply ile aynıdır', () => { + assert.ok(Array.isArray(T.SCENE_KEYS)); + assert.ok(T.SCENE_KEYS.includes('visualizer')); + assert.ok(T.SCENE_KEYS.includes('modulation')); + assert.ok(T.SCENE_KEYS.includes('layers')); + assert.ok(T.SCENE_KEYS.includes('layerStack')); + assert.ok(typeof T.resetScene === 'function'); +});