/** * SFX voice test (dev tool, run with Node — no browser needed): * * node dev/sfx.test.mjs * * Exercises the shared SFX voice (js/utils/Sfx.js) with a stub scene: * playSfxOn() guard rails (master switch off, missing sound manager, * asset not in the audio cache, the happy path, extra play options like * loop) and the stop/status helpers (stopSfxOn / sfxPlayingOn). Also * checks the sfx.json keys follow the play-name convention (sfx_ * cache key) and point at files that exist. */ import { pathToFileURL, fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import fs from 'node:fs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const SFX = JSON.parse(fs.readFileSync(join(__dirname, '../data/sfx.json'), 'utf8')); const { config } = await import(pathToFileURL(join(__dirname, '../js/config/Config.js')).href); config.init({ sfx: { ...SFX } }); const { playSfxOn, stopSfxOn, sfxPlayingOn } = await import(pathToFileURL(join(__dirname, '../js/utils/Sfx.js')).href); let failures = 0; const check = (label, cond) => { console.log(`${cond ? '✔' : '✘ FAIL'} ${label}`); if (!cond) failures++; }; /** A stub scene: sound manager + audio cache, recording play() calls. */ const makeScene = (o = {}) => { const calls = []; const stops = []; let playing = false; return { calls, stops, sound: o.sound === false ? null : { play: (key, opts) => { calls.push({ key, opts }); playing = true; }, stopByKey: (key) => { stops.push(key); playing = false; }, isPlaying: (key) => playing && key === calls[calls.length - 1]?.key, }, cache: o.noCache ? undefined : { audio: o.noAudio ? undefined : { has: (k) => (o.inCache === false ? false : true) } }, }; }; // 1) Happy path: plays the sfx_ key at sfx.volume. { const scene = makeScene(); playSfxOn(scene, 'ui_click'); check('plays sfx_ui_click at sfx.volume', scene.calls.length === 1 && scene.calls[0].key === 'sfx_ui_click' && scene.calls[0].opts.volume === SFX.volume); } // 2) Master switch off → silent, no throw. { const scene = makeScene(); config.init({ sfx: { ...SFX, enabled: false } }); playSfxOn(scene, 'ui_hover'); check('sfx.enabled=false → nothing plays', scene.calls.length === 0); config.init({ sfx: { ...SFX } }); } // 3) No sound manager (headless) → silent, no throw. { const scene = makeScene({ sound: false }); playSfxOn(scene, 'ui_click'); check('no sound manager → nothing plays', scene.calls.length === 0); } // 4) Asset not in the audio cache → silent, no throw (v4 play() would throw). { const scene = makeScene({ inCache: false }); playSfxOn(scene, 'ui_hover'); check('asset missing from cache → nothing plays', scene.calls.length === 0); } // 5) Cache object without an audio section → still plays (defensive guard). { const scene = makeScene({ noAudio: true }); playSfxOn(scene, 'ui_click'); check('cache without audio section → plays', scene.calls.length === 1); } // 6) data/sfx.json carries the UI ticks + the window whoosh + the close tick, pointing at real files. { const ok = ['ui_hover', 'ui_click', 'ui_window', 'ui_close'].every((k) => { const p = SFX[k]; return typeof p === 'string' && fs.existsSync(join(__dirname, '..', p)); }); check('sfx.json ui_hover/ui_click/ui_window/ui_close point at files that exist', ok); } // 7) Extra play options (the mining hum's loop) pass through after the // configured volume. { const scene = makeScene(); playSfxOn(scene, 'mining_loop', { loop: true }); check('playSfxOn passes extra options (loop) through with the sfx.volume default', scene.calls.length === 1 && scene.calls[0].key === 'sfx_mining_loop' && scene.calls[0].opts.loop === true && scene.calls[0].opts.volume === SFX.volume); } // 8) stopSfxOn stops by the sfx_ cache key (v4 has no sound.stop()). { const scene = makeScene(); playSfxOn(scene, 'mining_loop', { loop: true }); stopSfxOn(scene, 'mining_loop'); check('stopSfxOn stops the sfx_ key', scene.stops.length === 1 && scene.stops[0] === 'sfx_mining_loop'); const noMgr = makeScene({ sound: false }); let threw = false; try { stopSfxOn(noMgr, 'mining_loop'); } catch (e) { threw = true; } check('stopSfxOn with no sound manager → no throw', !threw); } // 9) sfxPlayingOn mirrors the manager's isPlaying (false when unsure). { const scene = makeScene(); check('sfxPlayingOn is false before anything plays', sfxPlayingOn(scene, 'mining_loop') === false); playSfxOn(scene, 'mining_loop', { loop: true }); check('sfxPlayingOn is true while it plays', sfxPlayingOn(scene, 'mining_loop') === true); stopSfxOn(scene, 'mining_loop'); check('sfxPlayingOn is false after stop', sfxPlayingOn(scene, 'mining_loop') === false); const noMgr = makeScene({ sound: false }); check('sfxPlayingOn with no sound manager → false (no throw)', sfxPlayingOn(noMgr, 'mining_loop') === false); } // 10) The mining_loop key points at a real file. { const p = SFX.mining_loop; check('sfx.json mining_loop points at a file that exists', typeof p === 'string' && fs.existsSync(join(__dirname, '..', p))); } console.log(failures === 0 ? '\nAll SFX voice tests passed ✔' : `\n${failures} test(s) FAILED ✘`); process.exit(failures === 0 ? 0 : 1);