import test from "node:test"; import assert from "node:assert/strict"; import { announcePassingHelper, announceRaceStartedHelper, getSessionAnnouncementText, handleSessionTimerTickHelper, selectSpeechVoice, } 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("session announcements distinguish practice from racing", () => { const t = (key) => key; assert.equal(getSessionAnnouncementText({ type: "open_practice" }, "start", { t }), "timing.practice_started_voice"); assert.equal(getSessionAnnouncementText({ type: "practice" }, "finish", { t }), "timing.practice_finished_voice"); assert.equal(getSessionAnnouncementText({ type: "final" }, "start", { t }), "timing.race_started_voice"); assert.equal(getSessionAnnouncementText({ type: "heat" }, "finish", { t }), "timing.race_finished_voice"); }); test("passing announcement uses the driver's spoken name and beeps while speech is busy", () => { const calls = []; announcePassingHelper( { displayName: "Anders", speechName: "Ann-ders" }, { state: { settings: { audioEnabled: true, passingSoundMode: "name" } }, t: () => "Unknown", speakText: (value, options) => { calls.push([value, options]); return false; }, playPassingBeep: () => calls.push(["beep"]), } ); assert.deepEqual(calls, [["Ann-ders", { interrupt: false }], ["beep"]]); }); test("race start creates an overlay event and plays enabled start cues", async () => { const calls = []; announceRaceStartedHelper( { id: "session_1", name: "A Final", type: "final" }, { state: { settings: { audioEnabled: true, startSoundEnabled: true, finishVoiceEnabled: true } }, t: (key) => key, pushOverlayEvent: (type, label, options) => calls.push([type, label, options]), playStartCue: () => calls.push(["tone"]), speakText: (value, options) => calls.push(["speech", value, options]), } ); assert.deepEqual(calls, [ ["start", "A Final • timing.start", { silentCue: true }], ["tone"], ]); await new Promise((resolve) => setTimeout(resolve, 700)); assert.deepEqual(calls[2], ["speech", "timing.race_started_voice", { interrupt: 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); });