From d47883c5e597a43cb5fde601fab186e7755080be Mon Sep 17 00:00:00 2001 From: Byron Gamatos Date: Sat, 11 Jul 2026 19:37:44 +0200 Subject: [PATCH] refactor(app): carve the tuning-display helpers out of app.js (R3a) (#884) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit static/js/tuning-display.js (228 lines) — bodies VERBATIM. app.js 9,838 → 9,650. A LEAF: imports nothing. Tuning NAME resolution (Drop D / Eb Standard / raw-offset fallback), bass detection, effective string count, and the target FREQUENCIES + note names the tuner checks against. Pure functions over a small MIDI/note-name table; the 3 _TUNING_* tables are read nowhere else and move in. NOT A SLICE — a node-level extract. The span 2309-2535 INTERLEAVES the functions with the `window.*` / `window.feedBack.*` assignments that publish them, and one of those is `window.feedBack = window.feedBack || {}` — the BUS BOOTSTRAP, not tuning code at all. Every ExpressionStatement stays exactly where it was; only the 16 functions and 3 tables move. app.js re-exposes the imported bindings from the same lines, so the public surface and its ordering are untouched (constitution II names window.feedBack). app.js -> { plugin-loader, viz, diagnostics-export, dom, highway-colors, tuning-display } HARNESSES — 4 broke, and 3 of them broke in the SAME informative way: they sliced app.js from `function isBassArrangement(` UP TO the marker `window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;` — an end-marker that (correctly) stayed behind in app.js. The module is now nothing BUT the tuning helpers, so there is no block to slice: they read it whole and strip `export ` so the vm sandbox still evaluates it as a script. tuner_auto_open is SPLIT — its autoplay-gate test still reads app.js, so it keeps APP_JS and gains TUNING_JS. Retargeting its path wholesale (my first attempt) silently pointed the autoplay test at the wrong file. VERIFIED BY DRIVING THE CONTRACT. A/B against origin/main in two browsers, through the real window surface: displayTuningName -> "E Standard" / "Drop D" / "Eb Standard", parseRawTuningOffsets('-2,0,0,0,0,0') -> [-2,0,0,0,0,0], isBassArrangement, effectiveStringCount, displayTuningTargets, and window.feedBack.displayTuningName / .songTuningContext — IDENTICAL on both, zero console/page errors either side. pytest 2396, node 1038/1038, ESLint 0, tailwind-fresh clean, Codex 0. Co-authored-by: Claude Opus 4.8 (1M context) --- static/app.js | 205 ++------------------------- static/js/tuning-display.js | 228 +++++++++++++++++++++++++++++++ tests/js/tuner_auto_open.test.js | 15 +- tests/js/tuning_display.test.js | 3 +- tests/js/tuning_targets.test.js | 13 +- tests/js/v3_songs_tuning.test.js | 13 +- 6 files changed, 262 insertions(+), 215 deletions(-) create mode 100644 static/js/tuning-display.js diff --git a/static/app.js b/static/app.js index bcaeb50..0dfb7c5 100644 --- a/static/app.js +++ b/static/app.js @@ -24,6 +24,15 @@ import { hwcInitSettingsUI, initHighwayColors, } from './js/highway-colors.js'; +import { + displayTuningName, + displayTuningTargetDetails, + displayTuningTargets, + effectiveStringCount, + isBassArrangement, + parseRawTuningOffsets, + songTuningContext, +} from './js/tuning-display.js'; // Demo analytics — real impl set by demo.js; no-op in normal builds window.feedBackDemoTrack = window.feedBackDemoTrack ?? null; @@ -2305,227 +2314,31 @@ function toggleAllFavoriteArtists(expand) { _toggleAllInTree('fav-tree', expand, _FAV_TREE_EXPAND_KEY); } -// Display-only tuning label helpers — never mutate offsets or affect playback. -function _looksLikeRawTuningOffsets(str) { - if (!str || typeof str !== 'string') return false; - const s = str.trim(); - if (!s) return false; - if (/^-?\d+$/.test(s)) return true; - if (/^-?\d+(?: -?\d+)+$/.test(s)) return true; - if (/^-?\d+(?:,-?\d+)+$/.test(s)) return true; - if (/^-?\d+(-?\d+){2,}$/.test(s)) return true; - return false; -} -function _tuningNameFromOffsets(offsets) { - if (!offsets || !offsets.length) return ''; - const standard = { - 0: 'E Standard', '-1': 'Eb Standard', '-2': 'D Standard', - '-3': 'C# Standard', '-4': 'C Standard', '-5': 'B Standard', - '-6': 'Bb Standard', '-7': 'A Standard', - 1: 'F Standard', 2: 'F# Standard', - }; - // Uniform offsets across 4 (bass) / 5 / 6 strings name the same Standard; - // a 4-string bass [0,0,0,0] must read "E Standard", not "Custom Tuning". - if (offsets.length >= 4 && offsets.every((o) => o === offsets[0])) { - const name = standard[offsets[0]]; - if (name) return name; - } - if (offsets.length >= 4 && offsets[0] === offsets[1] - 2 - && offsets.slice(1).every((o) => o === offsets[1])) { - const noteNames = ['E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B', 'C', 'C#', 'D', 'Eb']; - return 'Drop ' + noteNames[((offsets[0] % 12) + 12) % 12]; - } - const named = { - '-2,0,0,0,0,0': 'Drop D', - '-4,-2,-2,-2,-2,-2': 'Drop C', - '-2,-2,0,0,0,0': 'Double Drop D', - '0,0,0,-1,0,0': 'Open G', - '-2,-2,0,0,-2,-2': 'Open D', - '-2,0,0,0,-2,0': 'DADGAD', - '0,2,2,1,0,0': 'Open E', - '-2,0,0,2,3,2': 'Open D (alt)', - }; - if (offsets.length === 6) { - const key = offsets.join(','); - if (named[key]) return named[key]; - } - return 'Custom Tuning'; -} -function displayTuningName(value, offsets) { - // Explicit offsets win — always name them. - if (Array.isArray(offsets) && offsets.length > 0) { - return _tuningNameFromOffsets(offsets); - } - if (value && typeof value === 'string') { - const trimmed = value.trim(); - if (!trimmed || trimmed === 'Unknown') return ''; - if (!_looksLikeRawTuningOffsets(trimmed)) { - return trimmed; - } - // A raw offset string (now served by the API) — parse and name it so a - // known tuning like "-1 -1 -1 -1 -1 -1" reads "Eb Standard" rather than - // collapsing to "Custom Tuning". - const parsed = (typeof parseRawTuningOffsets === 'function') - ? parseRawTuningOffsets(trimmed) : null; - if (parsed && parsed.length) return _tuningNameFromOffsets(parsed); - return 'Custom Tuning'; - } - return ''; -} window.displayTuningName = displayTuningName; window.feedBack = window.feedBack || {}; window.slopsmith = window.feedBack; window.feedBack.displayTuningName = displayTuningName; -function isBassArrangement(context) { - const ctx = context && typeof context === 'object' ? context : {}; - if (typeof ctx.isBass === 'boolean') return ctx.isBass; - const label = ((ctx.arrangement || '') + ' ' + (ctx.arrangement_smart_name || '')).toLowerCase(); - if (/\bbass\b/.test(label)) return true; - if (/\b(lead|rhythm|combo|guitar)\b/.test(label)) return false; - return false; -} -function effectiveStringCount(offsets, context) { - if (!Array.isArray(offsets) || !offsets.length) return 0; - const ctx = context && typeof context === 'object' ? context : {}; - const isBass = isBassArrangement(ctx); - let sc = ctx.stringCount > 0 ? Number(ctx.stringCount) : 0; - if (!isBass) { - if (sc > 0 && sc <= 5 && offsets.length >= 6) sc = 6; - if (!sc) sc = offsets.length >= 6 ? offsets.length : 6; - } else if (!sc) { - sc = offsets.length >= 5 ? offsets.length : 4; - } - return Math.min(sc, offsets.length); -} -function songTuningContext(songInfo) { - if (!songInfo || typeof songInfo !== 'object') return {}; - return { - stringCount: songInfo.stringCount, - arrangement: songInfo.arrangement, - arrangement_smart_name: songInfo.arrangement_smart_name, - }; -} window.feedBack.isBassArrangement = isBassArrangement; window.feedBack.effectiveStringCount = effectiveStringCount; window.feedBack.songTuningContext = songTuningContext; -// Open-string target notes (display only) — mirrors plugins/tuner/utils/tuning-utils.js. -const _TUNING_BASE_MIDI = { - 4: [28, 33, 38, 43], - 5: [23, 28, 33, 38, 43], - 6: [40, 45, 50, 55, 59, 64], - 7: [35, 40, 45, 50, 55, 59, 64], - 8: [30, 35, 40, 45, 50, 55, 59, 64], -}; -const _TUNING_NOTE_SHARP = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; -const _TUNING_NOTE_FLAT = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; -function _tuningMidiToFreq(m) { - return Math.pow(2, (m - 69) / 12) * 440; -} -function _tuningOffsetsToFreqs(offsets, isBass) { - const len = offsets.length; - let base; - if (len === 4 || len === 5) { - base = isBass ? _TUNING_BASE_MIDI[len] : _TUNING_BASE_MIDI[6]; - } else { - base = _TUNING_BASE_MIDI[len] || _TUNING_BASE_MIDI[6]; - } - return offsets.map((offset, i) => { - const root = i < base.length ? base[i] : base[base.length - 1]; - return _tuningMidiToFreq(root + offset); - }); -} -function _noteNameFromFreq(freq, useFlats) { - const midi = 69 + 12 * Math.log2(freq / 440); - const rounded = Math.round(midi); - const names = useFlats ? _TUNING_NOTE_FLAT : _TUNING_NOTE_SHARP; - return names[((rounded % 12) + 12) % 12]; -} -function _octaveNoteFromFreq(freq, useFlats) { - const midi = 69 + 12 * Math.log2(freq / 440); - const rounded = Math.round(midi); - const octave = Math.floor(rounded / 12) - 1; - return _noteNameFromFreq(freq, useFlats) + octave; -} -function _stringOrdinalLabel(n) { - const v = n % 100; - if (v >= 11 && v <= 13) return n + 'th'; - const suffix = { 1: 'st', 2: 'nd', 3: 'rd' }[n % 10] || 'th'; - return n + suffix; -} -function _tuningTargetFreqs(offsets, context) { - if (!Array.isArray(offsets) || !offsets.length) return []; - const ctx = context && typeof context === 'object' ? context : {}; - const stringCount = effectiveStringCount(offsets, ctx); - const trimmed = offsets.slice(0, stringCount); - if (!trimmed.length) return []; - const isBass = isBassArrangement(ctx); - try { - return _tuningOffsetsToFreqs(trimmed, isBass); - } catch (_) { - return []; - } -} -// Flat vs sharp spelling. A caller that knows the preference can pass -// ctx.useFlats; otherwise we infer from a flat-keyed tuning name. The v3 -// card/HUD pass "Custom Tuning" (raw offsets carry no key), so those default -// to sharps unless an explicit useFlats is supplied. -function _resolveTargetUseFlats(ctx) { - if (typeof ctx.useFlats === 'boolean') return ctx.useFlats; - return typeof ctx.tuningName === 'string' && /\b[A-G]b\b/.test(ctx.tuningName); -} -function displayTuningTargetDetails(offsets, context) { - const ctx = context && typeof context === 'object' ? context : {}; - const useFlats = _resolveTargetUseFlats(ctx); - const freqs = _tuningTargetFreqs(offsets, ctx); - return freqs.map((f, i) => { - const stringNumber = freqs.length - i; - const note = _noteNameFromFreq(f, useFlats); - const octaveNote = _octaveNoteFromFreq(f, useFlats); - return { - stringNumber, - note, - octaveNote, - title: _stringOrdinalLabel(stringNumber) + ' string: ' + octaveNote, - }; - }); -} -function displayTuningTargets(offsets, context) { - const ctx = context && typeof context === 'object' ? context : {}; - const useFlats = _resolveTargetUseFlats(ctx); - const freqs = _tuningTargetFreqs(offsets, ctx); - if (!freqs.length) return ''; - return freqs.map((f) => _noteNameFromFreq(f, useFlats)).join(' '); -} -function parseRawTuningOffsets(value) { - if (Array.isArray(value) && value.length) return value; - if (!value || typeof value !== 'string') return null; - const s = value.trim(); - if (/^-?\d+(?: -?\d+)+$/.test(s)) { - return s.split(/\s+/).map((n) => Number(n)); - } - if (/^-?\d+(?:,-?\d+)+$/.test(s)) { - return s.split(',').map((n) => Number(n)); - } - return null; -} window.displayTuningTargets = displayTuningTargets; window.displayTuningTargetDetails = displayTuningTargetDetails; diff --git a/static/js/tuning-display.js b/static/js/tuning-display.js new file mode 100644 index 0000000..c7efe04 --- /dev/null +++ b/static/js/tuning-display.js @@ -0,0 +1,228 @@ +// Tuning display — naming, string counts, and target frequencies. +// +// Carved verbatim out of static/app.js (R3a). A LEAF module: imports nothing. +// +// Turns raw per-string semitone offsets into things a human reads: a tuning NAME +// ("Drop D", "Eb Standard", or a raw-offsets fallback), whether an arrangement is +// bass, its effective string count, and the target FREQUENCIES + note names the +// tuner checks against. Pure functions over a small MIDI/note-name table. +// +// The window / window.feedBack assignments for these stay in app.js — they are the +// public contract (constitution II names window.feedBack), and app.js re-exposes +// the imported bindings from exactly where it always did, so nothing about the +// surface or its ordering changes. + +// Display-only tuning label helpers — never mutate offsets or affect playback. +function _looksLikeRawTuningOffsets(str) { + if (!str || typeof str !== 'string') return false; + const s = str.trim(); + if (!s) return false; + if (/^-?\d+$/.test(s)) return true; + if (/^-?\d+(?: -?\d+)+$/.test(s)) return true; + if (/^-?\d+(?:,-?\d+)+$/.test(s)) return true; + if (/^-?\d+(-?\d+){2,}$/.test(s)) return true; + return false; +} + +function _tuningNameFromOffsets(offsets) { + if (!offsets || !offsets.length) return ''; + const standard = { + 0: 'E Standard', '-1': 'Eb Standard', '-2': 'D Standard', + '-3': 'C# Standard', '-4': 'C Standard', '-5': 'B Standard', + '-6': 'Bb Standard', '-7': 'A Standard', + 1: 'F Standard', 2: 'F# Standard', + }; + // Uniform offsets across 4 (bass) / 5 / 6 strings name the same Standard; + // a 4-string bass [0,0,0,0] must read "E Standard", not "Custom Tuning". + if (offsets.length >= 4 && offsets.every((o) => o === offsets[0])) { + const name = standard[offsets[0]]; + if (name) return name; + } + if (offsets.length >= 4 && offsets[0] === offsets[1] - 2 + && offsets.slice(1).every((o) => o === offsets[1])) { + const noteNames = ['E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B', 'C', 'C#', 'D', 'Eb']; + return 'Drop ' + noteNames[((offsets[0] % 12) + 12) % 12]; + } + const named = { + '-2,0,0,0,0,0': 'Drop D', + '-4,-2,-2,-2,-2,-2': 'Drop C', + '-2,-2,0,0,0,0': 'Double Drop D', + '0,0,0,-1,0,0': 'Open G', + '-2,-2,0,0,-2,-2': 'Open D', + '-2,0,0,0,-2,0': 'DADGAD', + '0,2,2,1,0,0': 'Open E', + '-2,0,0,2,3,2': 'Open D (alt)', + }; + if (offsets.length === 6) { + const key = offsets.join(','); + if (named[key]) return named[key]; + } + return 'Custom Tuning'; +} + +export function displayTuningName(value, offsets) { + // Explicit offsets win — always name them. + if (Array.isArray(offsets) && offsets.length > 0) { + return _tuningNameFromOffsets(offsets); + } + if (value && typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed || trimmed === 'Unknown') return ''; + if (!_looksLikeRawTuningOffsets(trimmed)) { + return trimmed; + } + // A raw offset string (now served by the API) — parse and name it so a + // known tuning like "-1 -1 -1 -1 -1 -1" reads "Eb Standard" rather than + // collapsing to "Custom Tuning". + const parsed = (typeof parseRawTuningOffsets === 'function') + ? parseRawTuningOffsets(trimmed) : null; + if (parsed && parsed.length) return _tuningNameFromOffsets(parsed); + return 'Custom Tuning'; + } + return ''; +} + +export function isBassArrangement(context) { + const ctx = context && typeof context === 'object' ? context : {}; + if (typeof ctx.isBass === 'boolean') return ctx.isBass; + const label = ((ctx.arrangement || '') + ' ' + (ctx.arrangement_smart_name || '')).toLowerCase(); + if (/\bbass\b/.test(label)) return true; + if (/\b(lead|rhythm|combo|guitar)\b/.test(label)) return false; + return false; +} + +export function effectiveStringCount(offsets, context) { + if (!Array.isArray(offsets) || !offsets.length) return 0; + const ctx = context && typeof context === 'object' ? context : {}; + const isBass = isBassArrangement(ctx); + let sc = ctx.stringCount > 0 ? Number(ctx.stringCount) : 0; + if (!isBass) { + if (sc > 0 && sc <= 5 && offsets.length >= 6) sc = 6; + if (!sc) sc = offsets.length >= 6 ? offsets.length : 6; + } else if (!sc) { + sc = offsets.length >= 5 ? offsets.length : 4; + } + return Math.min(sc, offsets.length); +} + +export function songTuningContext(songInfo) { + if (!songInfo || typeof songInfo !== 'object') return {}; + return { + stringCount: songInfo.stringCount, + arrangement: songInfo.arrangement, + arrangement_smart_name: songInfo.arrangement_smart_name, + }; +} + +// Open-string target notes (display only) — mirrors plugins/tuner/utils/tuning-utils.js. +const _TUNING_BASE_MIDI = { + 4: [28, 33, 38, 43], + 5: [23, 28, 33, 38, 43], + 6: [40, 45, 50, 55, 59, 64], + 7: [35, 40, 45, 50, 55, 59, 64], + 8: [30, 35, 40, 45, 50, 55, 59, 64], +}; + +const _TUNING_NOTE_SHARP = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; + +const _TUNING_NOTE_FLAT = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; + +function _tuningMidiToFreq(m) { + return Math.pow(2, (m - 69) / 12) * 440; +} + +function _tuningOffsetsToFreqs(offsets, isBass) { + const len = offsets.length; + let base; + if (len === 4 || len === 5) { + base = isBass ? _TUNING_BASE_MIDI[len] : _TUNING_BASE_MIDI[6]; + } else { + base = _TUNING_BASE_MIDI[len] || _TUNING_BASE_MIDI[6]; + } + return offsets.map((offset, i) => { + const root = i < base.length ? base[i] : base[base.length - 1]; + return _tuningMidiToFreq(root + offset); + }); +} + +function _noteNameFromFreq(freq, useFlats) { + const midi = 69 + 12 * Math.log2(freq / 440); + const rounded = Math.round(midi); + const names = useFlats ? _TUNING_NOTE_FLAT : _TUNING_NOTE_SHARP; + return names[((rounded % 12) + 12) % 12]; +} + +function _octaveNoteFromFreq(freq, useFlats) { + const midi = 69 + 12 * Math.log2(freq / 440); + const rounded = Math.round(midi); + const octave = Math.floor(rounded / 12) - 1; + return _noteNameFromFreq(freq, useFlats) + octave; +} + +function _stringOrdinalLabel(n) { + const v = n % 100; + if (v >= 11 && v <= 13) return n + 'th'; + const suffix = { 1: 'st', 2: 'nd', 3: 'rd' }[n % 10] || 'th'; + return n + suffix; +} + +function _tuningTargetFreqs(offsets, context) { + if (!Array.isArray(offsets) || !offsets.length) return []; + const ctx = context && typeof context === 'object' ? context : {}; + const stringCount = effectiveStringCount(offsets, ctx); + const trimmed = offsets.slice(0, stringCount); + if (!trimmed.length) return []; + const isBass = isBassArrangement(ctx); + try { + return _tuningOffsetsToFreqs(trimmed, isBass); + } catch (_) { + return []; + } +} + +// Flat vs sharp spelling. A caller that knows the preference can pass +// ctx.useFlats; otherwise we infer from a flat-keyed tuning name. The v3 +// card/HUD pass "Custom Tuning" (raw offsets carry no key), so those default +// to sharps unless an explicit useFlats is supplied. +function _resolveTargetUseFlats(ctx) { + if (typeof ctx.useFlats === 'boolean') return ctx.useFlats; + return typeof ctx.tuningName === 'string' && /\b[A-G]b\b/.test(ctx.tuningName); +} + +export function displayTuningTargetDetails(offsets, context) { + const ctx = context && typeof context === 'object' ? context : {}; + const useFlats = _resolveTargetUseFlats(ctx); + const freqs = _tuningTargetFreqs(offsets, ctx); + return freqs.map((f, i) => { + const stringNumber = freqs.length - i; + const note = _noteNameFromFreq(f, useFlats); + const octaveNote = _octaveNoteFromFreq(f, useFlats); + return { + stringNumber, + note, + octaveNote, + title: _stringOrdinalLabel(stringNumber) + ' string: ' + octaveNote, + }; + }); +} + +export function displayTuningTargets(offsets, context) { + const ctx = context && typeof context === 'object' ? context : {}; + const useFlats = _resolveTargetUseFlats(ctx); + const freqs = _tuningTargetFreqs(offsets, ctx); + if (!freqs.length) return ''; + return freqs.map((f) => _noteNameFromFreq(f, useFlats)).join(' '); +} + +export function parseRawTuningOffsets(value) { + if (Array.isArray(value) && value.length) return value; + if (!value || typeof value !== 'string') return null; + const s = value.trim(); + if (/^-?\d+(?: -?\d+)+$/.test(s)) { + return s.split(/\s+/).map((n) => Number(n)); + } + if (/^-?\d+(?:,-?\d+)+$/.test(s)) { + return s.split(',').map((n) => Number(n)); + } + return null; +} diff --git a/tests/js/tuner_auto_open.test.js b/tests/js/tuner_auto_open.test.js index c2f0542..9115a2b 100644 --- a/tests/js/tuner_auto_open.test.js +++ b/tests/js/tuner_auto_open.test.js @@ -7,20 +7,23 @@ const path = require('node:path'); const vm = require('node:vm'); const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js'); +// The tuning-display helpers were carved out of app.js into their own module (R3a); +// the autoplay-gate test below still reads app.js. +const TUNING_JS = path.join(__dirname, '..', '..', 'static', 'js', 'tuning-display.js'); const TUNER_SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'screen.js'); const TUNING_UTILS_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'tuning-utils.js'); const TUNER_UI_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'ui.js'); function loadTuningHelpers() { - const src = fs.readFileSync(APP_JS, 'utf8'); - const start = src.indexOf('function isBassArrangement('); - const endMarker = 'window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;'; - const end = src.indexOf(endMarker); - if (start === -1 || end === -1) throw new Error('tuning helper block not found in app.js'); + const src = fs.readFileSync(TUNING_JS, 'utf8'); + // The module is nothing BUT the tuning helpers now, so there is no block to + // slice out — take it whole. `export` is stripped so the vm sandbox can still + // evaluate it as a plain script (the window.* contract lives in app.js). + const body = src.replace(/^export /gm, ''); const sandbox = { window: { feedBack: {} }, exports: {} }; vm.createContext(sandbox); vm.runInContext( - src.slice(start, end + endMarker.length), + body, sandbox ); return sandbox.window.feedBack; diff --git a/tests/js/tuning_display.test.js b/tests/js/tuning_display.test.js index c67d3b8..6e8aa5c 100644 --- a/tests/js/tuning_display.test.js +++ b/tests/js/tuning_display.test.js @@ -6,7 +6,8 @@ const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); -const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js'); +// The tuning-display helpers were carved out of app.js into their own module (R3a). +const APP_JS = path.join(__dirname, '..', '..', 'static', 'js', 'tuning-display.js'); const HIGHWAY_JS = path.join(__dirname, '..', '..', 'static', 'highway.js'); const V3_HTML = path.join(__dirname, '..', '..', 'static', 'v3', 'index.html'); diff --git a/tests/js/tuning_targets.test.js b/tests/js/tuning_targets.test.js index 8cc070a..b027dec 100644 --- a/tests/js/tuning_targets.test.js +++ b/tests/js/tuning_targets.test.js @@ -6,7 +6,8 @@ const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); -const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js'); +// The tuning-display helpers were carved out of app.js into their own module (R3a). +const APP_JS = path.join(__dirname, '..', '..', 'static', 'js', 'tuning-display.js'); const HIGHWAY_JS = path.join(__dirname, '..', '..', 'static', 'highway.js'); const TUNER_UI_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'ui.js'); const TUNER_SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'screen.js'); @@ -14,14 +15,14 @@ const V3_HTML = path.join(__dirname, '..', '..', 'static', 'v3', 'index.html'); function loadTuningHelpers() { const src = fs.readFileSync(APP_JS, 'utf8'); - const start = src.indexOf('function isBassArrangement('); - const endMarker = 'window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;'; - const end = src.indexOf(endMarker); - if (start === -1 || end === -1) throw new Error('tuning helper block not found in app.js'); + // The module is nothing BUT the tuning helpers now, so there is no block to + // slice out — take it whole. `export` is stripped so the vm sandbox can still + // evaluate it as a plain script (the window.* contract lives in app.js). + const body = src.replace(/^export /gm, ''); const sandbox = { window: { feedBack: {} }, exports: {} }; vm.createContext(sandbox); vm.runInContext( - src.slice(start, end + endMarker.length) + '\n' + body + '\n' + 'exports.displayTuningTargets = displayTuningTargets;\n' + 'exports.displayTuningTargetDetails = displayTuningTargetDetails;\n' + 'exports.isBassArrangement = isBassArrangement;\n' diff --git a/tests/js/v3_songs_tuning.test.js b/tests/js/v3_songs_tuning.test.js index 2d39860..913f472 100644 --- a/tests/js/v3_songs_tuning.test.js +++ b/tests/js/v3_songs_tuning.test.js @@ -7,7 +7,8 @@ const path = require('node:path'); const vm = require('node:vm'); const SONGS_JS = path.join(__dirname, '..', '..', 'static', 'v3', 'songs.js'); -const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js'); +// The tuning-display helpers were carved out of app.js into their own module (R3a). +const APP_JS = path.join(__dirname, '..', '..', 'static', 'js', 'tuning-display.js'); function extractBlock(src, startMarker) { const start = src.indexOf(startMarker); @@ -27,14 +28,14 @@ function extractBlock(src, startMarker) { function loadTuningHelpers() { const src = fs.readFileSync(APP_JS, 'utf8'); - const start = src.indexOf('function _looksLikeRawTuningOffsets('); - const endMarker = 'window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;'; - const end = src.indexOf(endMarker); - if (start === -1 || end === -1) throw new Error('tuning helpers not found'); + // The module is nothing BUT the tuning helpers now, so there is no block to + // slice out — take it whole. `export` is stripped so the vm sandbox can still + // evaluate it as a plain script (the window.* contract lives in app.js). + const body = src.replace(/^export /gm, ''); const sandbox = { window: { feedBack: {} }, exports: {} }; vm.createContext(sandbox); vm.runInContext( - src.slice(start, end + endMarker.length) + '\n' + body + '\n' + 'exports.displayTuningName = displayTuningName;\n' + 'exports.displayTuningTargets = displayTuningTargets;\n' + 'exports.parseRawTuningOffsets = parseRawTuningOffsets;',