From 106272b6e34cba8c27462aa786803f70c3caef06 Mon Sep 17 00:00:00 2001 From: CaYatur <152031095+CaYatur@users.noreply.github.com> Date: Sat, 26 Sep 2026 13:36:03 +0300 Subject: [PATCH 1/2] test(admin): cover #sections scroll restore (#615/#616) Source contract + behavioral simulation (fake element, controllable rAF) for gen++, double rAF, clamp, and isConnected skip. --- tests/admin-sections-scroll.test.js | 158 ++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/admin-sections-scroll.test.js diff --git a/tests/admin-sections-scroll.test.js b/tests/admin-sections-scroll.test.js new file mode 100644 index 0000000..d95cebf --- /dev/null +++ b/tests/admin-sections-scroll.test.js @@ -0,0 +1,158 @@ +'use strict'; +/* #615 / #616: Admin #sections scroll, render() sonrası sıfırlanmamalı. + * + * DOM/Electron olmadan iki katman doğrulanır: + * 1) Kaynak sözleşmesi — admin.js içinde gen++, çift rAF, clamp, isConnected. + * 2) Davranış — restore algoritması sahte eleman + kontrollü rAF kuyruğu ile. + */ +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); + +const adminSrc = fs.readFileSync( + path.join(__dirname, '..', 'src', 'admin', 'admin.js'), + 'utf8' +); + +test('kaynak: #615 sections scroll restore deseni admin.js içinde', () => { + assert.match(adminSrc, /let sectionsScrollRestoreGen\s*=\s*0/); + assert.match(adminSrc, /const gen\s*=\s*\+\+sectionsScrollRestoreGen/); + assert.match(adminSrc, /const restoreSectionsScroll\s*=\s*\(\)\s*=>/); + assert.match( + adminSrc, + /if\s*\(\s*gen\s*!==\s*sectionsScrollRestoreGen\s*\|\|\s*!root\.isConnected\s*\)\s*return/ + ); + assert.match( + adminSrc, + /const max\s*=\s*Math\.max\(\s*0\s*,\s*root\.scrollHeight\s*-\s*root\.clientHeight\s*\)/ + ); + assert.match(adminSrc, /root\.scrollTop\s*=\s*Math\.min\(\s*prevScroll\s*,\s*max\s*\)/); + assert.match( + adminSrc, + /requestAnimationFrame\(\s*\(\)\s*=>\s*\{\s*restoreSectionsScroll\(\);\s*requestAnimationFrame\(restoreSectionsScroll\);\s*\}\)/ + ); +}); + +/** admin.js ile aynı restore algoritması; rAF enjekte edilebilir. */ +function makeRestore(root, prevScroll, opts) { + const state = opts.state; + const raf = opts.raf; + const gen = ++state.gen; + const restoreSectionsScroll = () => { + if (gen !== state.gen || !root.isConnected) return; + const max = Math.max(0, root.scrollHeight - root.clientHeight); + root.scrollTop = Math.min(prevScroll, max); + }; + restoreSectionsScroll(); + raf(() => { + restoreSectionsScroll(); + raf(restoreSectionsScroll); + }); + return { gen, restoreSectionsScroll }; +} + +function fakeRoot(init) { + return { + scrollTop: init.scrollTop ?? 0, + scrollHeight: init.scrollHeight ?? 0, + clientHeight: init.clientHeight ?? 0, + isConnected: init.isConnected !== false, + }; +} + +function makeRafQueue() { + const q = []; + const raf = (fn) => { + q.push(fn); + return q.length; + }; + const flushOne = () => { + assert.ok(q.length > 0, 'rAF kuyruğu boş'); + const fn = q.shift(); + fn(); + }; + const flushAll = () => { + while (q.length) flushOne(); + }; + return { raf, flushOne, flushAll, queue: q }; +} + +test('davranış: içerik yeniden kurulunca scroll korunur (yükseklik yeter)', () => { + const root = fakeRoot({ scrollTop: 240, scrollHeight: 800, clientHeight: 400 }); + const prevScroll = root.scrollTop; + // rebuild: içerik temizlenip yeniden dolduruluyor; scrollTop sıfırlanabilir + root.scrollTop = 0; + root.scrollHeight = 800; + root.clientHeight = 400; + + const state = { gen: 0 }; + const { raf, flushAll } = makeRafQueue(); + makeRestore(root, prevScroll, { state, raf }); + assert.strictEqual(root.scrollTop, 240, 'sync restore hemen uygulamalı'); + flushAll(); + assert.strictEqual(root.scrollTop, 240, 'çift rAF sonrası scroll aynı kalmalı'); +}); + +test('davranış: max küçülünce clamp edilir', () => { + const root = fakeRoot({ scrollTop: 500, scrollHeight: 1000, clientHeight: 400 }); + const prevScroll = root.scrollTop; + root.scrollTop = 0; + // yeniden kurulum sonrası daha kısa içerik + root.scrollHeight = 500; + root.clientHeight = 400; + const max = Math.max(0, root.scrollHeight - root.clientHeight); // 100 + + const state = { gen: 0 }; + const { raf, flushAll } = makeRafQueue(); + makeRestore(root, prevScroll, { state, raf }); + assert.strictEqual(root.scrollTop, max); + flushAll(); + assert.strictEqual(root.scrollTop, 100); +}); + +test('davranış: sonraki render\'ın gen\'i eski rAF restore\'unu ezer', () => { + const root = fakeRoot({ scrollTop: 300, scrollHeight: 900, clientHeight: 400 }); + const state = { gen: 0 }; + const { raf, flushOne, queue } = makeRafQueue(); + + // ilk render: prev=300 + makeRestore(root, 300, { state, raf }); + assert.strictEqual(root.scrollTop, 300); + assert.strictEqual(queue.length, 1, 'ilk rAF planlandı'); + + // kullanıcı kaydırdı / ikinci render yeni prev ile + root.scrollTop = 50; + makeRestore(root, 50, { state, raf }); + assert.strictEqual(root.scrollTop, 50); + assert.strictEqual(state.gen, 2); + + // eski (gen=1) dış rAF çalışır — stale, dokunmamalı + flushOne(); + assert.strictEqual(root.scrollTop, 50, 'eski gen scroll\'u ezmemeli'); + + // yeni render\'ın dış rAF'ı: restore + iç rAF + flushOne(); + assert.strictEqual(root.scrollTop, 50); + // iç rAF + flushOne(); + assert.strictEqual(root.scrollTop, 50); + // eski render\'ın iç rAF'ı (stale) kuyrukta kalmış olabilir + while (queue.length) flushOne(); + assert.strictEqual(root.scrollTop, 50); +}); + +test('davranış: isConnected false iken restore atlanır', () => { + const root = fakeRoot({ + scrollTop: 0, + scrollHeight: 800, + clientHeight: 400, + isConnected: false, + }); + const state = { gen: 0 }; + const { raf, flushAll } = makeRafQueue(); + makeRestore(root, 200, { state, raf }); + assert.strictEqual(root.scrollTop, 0, 'bağlı değilken sync restore yok'); + flushAll(); + assert.strictEqual(root.scrollTop, 0, 'bağlı değilken rAF restore yok'); +}); From bb05c666755a306bec2f93e521e98a4cd2247595 Mon Sep 17 00:00:00 2001 From: CaYaTur Date: Sat, 26 Sep 2026 13:37:20 +0300 Subject: [PATCH 2/2] docs: bump unit test count to 2202 for admin scroll tests --- README.md | 4 ++-- README.tr.md | 4 ++-- ROADMAP.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 225f49b..6a066cf 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-2197%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-2202%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1315,7 +1315,7 @@ npm test npm start -- --smoke ``` -**2197 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2202 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 ca5617b..35ffa38 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-2197%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-2202%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1313,7 +1313,7 @@ npm test npm start -- --smoke ``` -**2197 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: +**2202 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 fd7b1d2..89fe239 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ npm test npm start -- --smoke ``` -- **2197 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2202 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 600 on `main` since.