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);
+ }
+}