Adds 'Calls during a race' (default: passing tone only, like timing systems) that drops name/personal best/leader/position calls in race sessions; practice keeps them. Removes the legacy audio panel and fallbacks, including its 'Sound on' switch that silently muted every race-audio voice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
4.1 KiB
JavaScript
101 lines
4.1 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
announceRaceStartedHelper,
|
|
handleSessionTimerTickHelper,
|
|
selectSpeechVoice,
|
|
speakTextHelper,
|
|
} from "../src/runtime_services.js";
|
|
|
|
test("speech voice selection prefers an explicit voice and then the current language", () => {
|
|
const voices = [
|
|
{ name: "English", lang: "en-US", voiceURI: "voice-en", localService: true },
|
|
{ name: "Swedish", lang: "sv-SE", voiceURI: "voice-sv", localService: true },
|
|
];
|
|
|
|
assert.equal(selectSpeechVoice(voices, "voice-en", "sv-SE")?.voiceURI, "voice-en");
|
|
assert.equal(selectSpeechVoice(voices, "", "sv-SE")?.voiceURI, "voice-sv");
|
|
assert.equal(selectSpeechVoice(voices, "missing", "de-DE"), null);
|
|
});
|
|
|
|
test("race start only creates the overlay event - the old start tone and voice are gone (race audio owns the start)", async () => {
|
|
const calls = [];
|
|
announceRaceStartedHelper(
|
|
{ id: "session_1", name: "A Final", type: "final" },
|
|
{
|
|
// Old saved settings from the removed legacy panel must not bring the extra cues back.
|
|
state: { settings: { audioEnabled: true, startSoundEnabled: true, finishVoiceEnabled: true, raceAudio: { enabled: false } } },
|
|
t: (key) => key,
|
|
pushOverlayEvent: (type, label, options) => calls.push([type, label, options]),
|
|
playStartCue: () => calls.push(["tone"]),
|
|
speakText: (value, options) => calls.push(["speech", value, options]),
|
|
}
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
assert.deepEqual(calls, [["start", "A Final • timing.start", { silentCue: true }]]);
|
|
});
|
|
|
|
test("session timer emits each race-audio clock marker once without changing finish rules", () => {
|
|
const active = { id: "s1", eventId: "e1", type: "final", status: "running", followUpSec: 30, audioAnnouncements: [] };
|
|
const announced = [];
|
|
let remainingMs = 59_000;
|
|
let saves = 0;
|
|
const run = () => handleSessionTimerTickHelper({
|
|
getActiveSession: () => active,
|
|
isUntimedSession: () => false,
|
|
getSessionTiming: () => ({ remainingMs, followUpRemainingMs: 30_000 }),
|
|
saveState: () => { saves += 1; },
|
|
getLastFinishAnnouncementSessionId: () => null,
|
|
setLastFinishAnnouncementSessionId: () => {},
|
|
announceRaceFinished: () => announced.push("race_finished"),
|
|
announceRaceClockEvent: (key) => announced.push(key),
|
|
});
|
|
run(); run();
|
|
remainingMs = 29_000; run();
|
|
remainingMs = 9_000; run();
|
|
remainingMs = 0; run();
|
|
assert.deepEqual(announced, ["one_minute_remaining", "thirty_seconds_remaining", "ten_seconds_remaining", "time_expired", "final_lap"]);
|
|
assert.equal(active.status, "running");
|
|
assert.ok(active.followUpStartedAt);
|
|
assert.ok(saves >= 4);
|
|
});
|
|
|
|
test("race audio can ignore a mismatched configured voice and select the announcement language", () => {
|
|
const originalWindow = globalThis.window;
|
|
const OriginalUtterance = globalThis.SpeechSynthesisUtterance;
|
|
const spoken = [];
|
|
const voices = [
|
|
{ name: "English", lang: "en-US", voiceURI: "voice-en", localService: true },
|
|
{ name: "Swedish", lang: "sv-SE", voiceURI: "voice-sv", localService: true },
|
|
];
|
|
globalThis.window = {
|
|
speechSynthesis: {
|
|
speaking: false,
|
|
pending: false,
|
|
cancel() {},
|
|
getVoices: () => voices,
|
|
speak: (utterance) => spoken.push(utterance),
|
|
},
|
|
};
|
|
globalThis.SpeechSynthesisUtterance = class {
|
|
constructor(text) { this.text = text; }
|
|
};
|
|
try {
|
|
// audioEnabled false: the removed legacy "Sound on" switch used to silence every race-audio
|
|
// voice (while effects still played). It must no longer have any effect.
|
|
const started = speakTextHelper("Race finished", {
|
|
state: { settings: { audioEnabled: false, speechVoiceUri: "voice-sv", speechRate: 1, speechPitch: 1 } },
|
|
currentLanguage: () => "sv",
|
|
}, { language: "en", voiceUri: "", rate: 1.1, pitch: 0.85 });
|
|
assert.equal(started, true);
|
|
assert.equal(spoken[0].lang, "en-US");
|
|
assert.equal(spoken[0].voice.voiceURI, "voice-en");
|
|
assert.equal(spoken[0].rate, 1.1);
|
|
assert.equal(spoken[0].pitch, 0.85);
|
|
} finally {
|
|
globalThis.window = originalWindow;
|
|
globalThis.SpeechSynthesisUtterance = OriginalUtterance;
|
|
}
|
|
});
|