orbit/dev/sfx.test.mjs

159 lines
5.4 KiB
JavaScript

/**
* 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_<key>
* 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);