using System; using System.IO; using System.Linq; using Microsoft.Data.Sqlite; using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.ViewModels; namespace ytLive.Tests; /// /// TASK 35 — the scene remembers the room. /// /// The rule the test pins, in the creator's words: a scene's audio state is applied once when /// it takes the program output. `null` means "leave the mix alone", so staging a scene offline, or /// switching to a scene with no opinion, must never undo a manual mute. /// [Collection("RealApp")] public sealed class SceneAudioTests { private readonly RealAppHost _app; public SceneAudioTests(RealAppHost app) => _app = app; [Fact] public void EnteringALiveScene_AppliesItsAudioState_OnceAndOnlyLive() => _app.Run(() => { var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-sceneaudio-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; try { var vm = new MainViewModel(); var brb = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Brb)); var chat = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Chat)); // Ships muted (the creator's default), while every other scene is passive. Assert.True(brb.SceneMutesMic); Assert.Null(chat.SceneMutesMic); Assert.Null(brb.SceneMutesDesktop); vm.MicVolume = 0.8; Assert.False(vm.MicMuted); // Staging offline: no program output, so no audio edit. The creator is auditioning. vm.TransitionToScene(brb); Assert.False(vm.MicMuted); // Live: entering BRB mutes the mic. vm.StreamStatus = StreamStatus.Streaming; vm.TransitionToScene(vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Live))); vm.TransitionToScene(brb); Assert.True(vm.MicMuted); // The creator is boss: a manual unmute survives a scene with no opinion. vm.SetMicMuted(false); Assert.False(vm.MicMuted); vm.TransitionToScene(chat); Assert.False(vm.MicMuted); // ...but a scene that DOES have an opinion wins on its next entry. chat.SceneMutesMic = true; vm.TransitionToScene(brb); vm.TransitionToScene(chat); Assert.True(vm.MicMuted); // Desktop audio was never told anything, so it was never touched. Assert.False(vm.GameMuted); // Roundtrip: the three states survive a save + reload, null included. vm.SaveLayoutNow(); SqliteConnection.ClearAllPools(); using var store = new LayoutStore(tempDb); var reloaded = store.Load(); var reloadedBrb = reloaded.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Brb)); var reloadedChat = reloaded.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Chat)); var reloadedLive = reloaded.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Live)); Assert.True(reloadedBrb.SceneMutesMic); Assert.True(reloadedChat.SceneMutesMic); Assert.Null(reloadedLive.SceneMutesMic); } finally { MainViewModel.LayoutPathOverride = null; SqliteConnection.ClearAllPools(); try { File.Delete(tempDb); } catch { /* best effort */ } } }); }