orbit/dev/sfx.test.mjs

203 lines
7.3 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 playSfxOneOfOn() (the one-of-a-set voice: a thunder strike,
* a solar flare) and that 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, playSfxOneOfOn, 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)));
}
// 11) EVERY sfx.json asset key points at a file that exists (the scene
// load list trusts the data; a typo would be a silent dead voice).
{
const keys = Object.entries(SFX)
.filter(([k, v]) => k !== '_comment' && typeof v === 'string');
const ok = keys.length > 0 && keys.every(([, v]) => fs.existsSync(join(__dirname, '..', v)));
check(`every sfx.json asset key (n=${keys.length}) points at a file that exists`, ok);
}
// 12) The new effect voices (thunder / solar) point at real files.
{
const ok = ['thunder_01', 'thunder_02', 'thunder_03', 'thunder_04',
'solar_buzz', 'solar_flare_01', 'solar_flare_02'].every((k) =>
typeof SFX[k] === 'string' && fs.existsSync(join(__dirname, '..', SFX[k])));
check('sfx.json thunder_01…04 + solar_buzz + solar_flare_01/02 point at files that exist', ok);
}
// 13) playSfxOneOfOn: exactly ONE member of the set plays, always a valid
// member, at the configured volume; empty/invalid lists stay silent.
{
const names = ['solar_flare_01', 'solar_flare_02'];
const valid = new Set(names.map((n) => `sfx_${n}`));
let ok = true;
for (let i = 0; i < 200 && ok; i++) {
const scene = makeScene();
playSfxOneOfOn(scene, names);
ok = scene.calls.length === 1 && valid.has(scene.calls[0].key) && scene.calls[0].opts.volume === SFX.volume;
}
check('playSfxOneOfOn plays exactly one valid member of the set (200 draws)', ok);
const empty = makeScene();
playSfxOneOfOn(empty, []);
check('playSfxOneOfOn with an empty set → nothing plays', empty.calls.length === 0);
const junk = makeScene();
playSfxOneOfOn(junk, 'not-a-list');
check('playSfxOneOfOn with a non-list → nothing plays (no throw)', junk.calls.length === 0);
const noMgr = makeScene({ sound: false });
playSfxOneOfOn(noMgr, names);
check('playSfxOneOfOn with no sound manager → nothing plays (no throw)', noMgr.calls.length === 0);
}
console.log(failures === 0 ? '\nAll SFX voice tests passed ✔' : `\n${failures} test(s) FAILED ✘`);
process.exit(failures === 0 ? 0 : 1);