diff --git a/DeepDrftPublic.Client/Controls/WaveformVisualizerControls.razor b/DeepDrftPublic.Client/Controls/WaveformVisualizerControls.razor index e8a6285..731ef36 100644 --- a/DeepDrftPublic.Client/Controls/WaveformVisualizerControls.razor +++ b/DeepDrftPublic.Client/Controls/WaveformVisualizerControls.razor @@ -226,12 +226,14 @@ private void ToggleLava() { ControlState.LavaEnabled = !ControlState.LavaEnabled; + ControlState.CoerceTheaterMode(); ControlState.NotifyChanged(); } private void ToggleWaveform() { ControlState.WaveformEnabled = !ControlState.WaveformEnabled; + ControlState.CoerceTheaterMode(); ControlState.NotifyChanged(); } diff --git a/DeepDrftPublic.Client/Services/WaveformVisualizerControlState.cs b/DeepDrftPublic.Client/Services/WaveformVisualizerControlState.cs index 556bf98..c20faa1 100644 --- a/DeepDrftPublic.Client/Services/WaveformVisualizerControlState.cs +++ b/DeepDrftPublic.Client/Services/WaveformVisualizerControlState.cs @@ -160,6 +160,19 @@ public sealed class WaveformVisualizerControlState /// public event Action? Changed; + /// + /// Enforces the Theater-Mode invariant: Theater Mode cannot remain on when both visualizer + /// subsystems are off (there is nothing to go to theater FOR). Call this after mutating + /// or and before + /// so all observers see a consistent, coerced state in the same + /// cycle. + /// + public void CoerceTheaterMode() + { + if (TheaterMode && !LavaEnabled && !WaveformEnabled) + TheaterMode = false; + } + /// Raise . Called by the controls component after mutating a value. public void NotifyChanged() => Changed?.Invoke(); } diff --git a/DeepDrftTests/WaveformVisualizerControlStateTests.cs b/DeepDrftTests/WaveformVisualizerControlStateTests.cs new file mode 100644 index 0000000..42c0664 --- /dev/null +++ b/DeepDrftTests/WaveformVisualizerControlStateTests.cs @@ -0,0 +1,91 @@ +using DeepDrftPublic.Client.Services; + +namespace DeepDrftTests; + +/// +/// Unit tests for the Theater-Mode auto-exit invariant on +/// (Phase 20 bug fix): when both subsystems are disabled, +/// must force TheaterMode = false so observers never see a stranded-theater state. +/// +[TestFixture] +public class WaveformVisualizerControlStateTests +{ + private WaveformVisualizerControlState _state = null!; + + [SetUp] + public void SetUp() => _state = new WaveformVisualizerControlState(); + + // ── CoerceTheaterMode guard ── + + // Both off + Theater on → coerce exits theater. + [Test] + public void CoerceTheaterMode_BothOff_TheaterBecomesfalse() + { + _state.TheaterMode = true; + _state.LavaEnabled = false; + _state.WaveformEnabled = false; + + _state.CoerceTheaterMode(); + + Assert.That(_state.TheaterMode, Is.False); + } + + // Lava still on → theater is left alone even if waveform is off. + [Test] + public void CoerceTheaterMode_LavaOnWaveformOff_TheaterPreserved() + { + _state.TheaterMode = true; + _state.LavaEnabled = true; + _state.WaveformEnabled = false; + + _state.CoerceTheaterMode(); + + Assert.That(_state.TheaterMode, Is.True); + } + + // Waveform still on → theater is left alone even if lava is off. + [Test] + public void CoerceTheaterMode_WaveformOnLavaOff_TheaterPreserved() + { + _state.TheaterMode = true; + _state.LavaEnabled = false; + _state.WaveformEnabled = true; + + _state.CoerceTheaterMode(); + + Assert.That(_state.TheaterMode, Is.True); + } + + // Theater already false + both off → no change (no false-positive write). + [Test] + public void CoerceTheaterMode_TheaterAlreadyFalse_NoChange() + { + _state.TheaterMode = false; + _state.LavaEnabled = false; + _state.WaveformEnabled = false; + + _state.CoerceTheaterMode(); + + Assert.That(_state.TheaterMode, Is.False); + } + + // ── Changed event fires once with coerced state visible ── + + // Verify that after coercion, the Changed notification carries the already-corrected TheaterMode + // value — all observers see a consistent state in the single Changed cycle. + [Test] + public void NotifyChanged_AfterCoerce_ObserverSeesTheaterFalse() + { + _state.TheaterMode = true; + _state.LavaEnabled = false; + _state.WaveformEnabled = false; + + bool? observedTheaterMode = null; + _state.Changed += () => observedTheaterMode = _state.TheaterMode; + + _state.CoerceTheaterMode(); + _state.NotifyChanged(); + + Assert.That(observedTheaterMode, Is.False); + } +}