Files
mrleo1nid 4dc660f786
CI / frontend (push) Failing after 20s
CI / backend (push) Failing after 36s
Fix MIST gallery scene controls, video positioning and audio overlap
2026-10-27 16:14:13 +03:00

139 lines
5.3 KiB
JavaScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { beforeEach, test } from "node:test";
import ts from "typescript";
// Exercise the production module with a deterministic media element and animation clock.
const source = readFileSync(new URL("../src/player/audio.ts", import.meta.url), "utf8");
const { outputText } = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } });
const { AudioSystem } = await import(`data:text/javascript;base64,${Buffer.from(outputText).toString("base64")}`);
let now, frames, elements;
beforeEach(() => {
now = 0;
frames = new Map();
elements = [];
let id = 0;
globalThis.performance = { now: () => now };
globalThis.requestAnimationFrame = (fn) => { frames.set(++id, fn); return id; };
globalThis.cancelAnimationFrame = (key) => frames.delete(key);
globalThis.Audio = class {
volume = 1;
paused = true;
constructor(src) { this.src = src; elements.push(this); }
play() { this.paused = false; return Promise.resolve(); }
pause() { this.paused = true; }
};
});
function tick(ms) {
now += ms;
const callbacks = [...frames.values()];
frames.clear();
callbacks.forEach((fn) => fn());
}
const volumes = () => ({ music: 0.7, sound: 0.8, voice: 1 });
const system = (registered = {}) => new AudioSystem(volumes(), (file) => file, registered);
test("custom mixers control only their channels and survive changes to shared volumes", () => {
const audio = system({ ambient: { mixer: "ambiant", loop: true }, ambient2: { mixer: "ambiant", loop: true } });
audio.play("ambient", ["one"], {});
audio.play("sound", ["effect"], {});
audio.setMixer("ambiant", 0.3);
assert.deepEqual(elements.map((el) => el.volume), [0.3, 0.8]);
audio.setVolumes({ music: 0, sound: 0.2, voice: 1 });
audio.play("ambient2", ["two"], {});
assert.deepEqual(elements.map((el) => el.volume), [0.3, 0.2, 0.3]);
assert.equal(audio.mixerVolume("ambiant"), 0.3);
});
test("volume changes apply immediately to each mixer and future tracks", () => {
const audio = system({ ambience: { mixer: "sfx", loop: true }, score: { mixer: "music", loop: true } });
for (const ch of ["music", "sound", "voice", "ambience", "score"]) audio.play(ch, [ch], {});
audio.setVolumes({ music: 0, sound: 0.25, voice: 0.5 });
assert.deepEqual(elements.map((el) => el.volume), [0, 0.25, 0.5, 0.25, 0]);
audio.play("music", ["next"], { fadeout: 0 });
assert.equal(elements.at(-1).volume, 0);
});
test("fade-in respects live volume changes and stays muted at zero", () => {
const audio = system();
audio.play("music", ["track"], { fadein: 2 });
tick(1000);
assert.equal(elements[0].volume, 0.35);
audio.setVolumes({ ...volumes(), music: 0.2 });
assert.equal(elements[0].volume, 0.1);
tick(500);
assert.ok(Math.abs(elements[0].volume - 0.15) < 1e-9);
audio.setVolumes({ ...volumes(), music: 0 });
tick(500);
assert.equal(elements[0].volume, 0);
});
test("script track gain multiplies the mixer and survives fades and queued clips", () => {
const audio = system();
audio.play("voice", ["first", "second"], { volume: 0.25, fadein: 2 });
tick(1000);
assert.equal(elements[0].volume, 0.125);
audio.setVolumes({ ...volumes(), voice: 0.4 });
assert.equal(elements[0].volume, 0.05);
tick(1000);
elements[0].onended();
assert.equal(elements.at(-1).src, "second");
assert.equal(elements.at(-1).volume, 0.1);
});
test("outgoing tracks also obey the mixer during crossfade", () => {
const audio = system();
audio.play("music", ["old"], {});
audio.play("music", ["new"], { fadeout: 2, fadein: 2 });
tick(500);
audio.setVolumes({ ...volumes(), music: 0 });
tick(500);
assert.deepEqual(elements.map((el) => el.volume), [0, 0]);
tick(1000);
assert.equal(elements[0].paused, true);
});
test("rapid crossfades keep only the current track and its immediate predecessor", () => {
const audio = system();
audio.play("sound", ["effect"], {});
for (const file of ["one", "two", "three", "four"]) {
audio.play("music", [file], { fadeout: 2, fadein: 2 });
tick(100);
}
assert.deepEqual(elements.filter(el => !el.paused).map(el => el.src), ["effect", "three", "four"]);
audio.stop("music");
assert.deepEqual(elements.filter(el => !el.paused).map(el => el.src), ["effect"]);
assert.equal(frames.size, 0);
});
test("stopping during fade-in cancels the old animation", () => {
const audio = system();
audio.play("music", ["track"], { fadein: 2 });
tick(500);
audio.stop("music", 0.5);
tick(500);
tick(1000);
assert.equal(elements[0].volume, 0);
assert.equal(elements[0].paused, true);
assert.equal(frames.size, 0);
});
test("stopAll stops outgoing audio and cancels every fade", () => {
const audio = system();
audio.play("music", ["old"], {});
audio.play("music", ["new"], { fadeout: 3, fadein: 3 });
audio.stopAll();
assert.ok(elements.every((el) => el.paused));
assert.equal(frames.size, 0);
assert.equal(audio.current("music"), null);
});
test("queued tracks use the latest mixer volume", () => {
const audio = system();
audio.play("sound", ["first", "second"], {});
audio.setVolumes({ ...volumes(), sound: 0.12 });
elements[0].onended();
assert.equal(elements[1].volume, 0.12);
assert.equal(audio.current("sound").file, "second");
});