fix(theater): auto-exit Theater Mode when both visualizer subsystems are disabled
Adds CoerceTheaterMode() to WaveformVisualizerControlState; ToggleLava/ToggleWaveform call it before NotifyChanged so all observers see consistent state in one Changed cycle. Covers the dead-end escape route bug (Phase 20 review finding).
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +160,19 @@ public sealed class WaveformVisualizerControlState
|
||||
/// </summary>
|
||||
public event Action? Changed;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="LavaEnabled"/> or <see cref="WaveformEnabled"/> and before
|
||||
/// <see cref="NotifyChanged"/> so all observers see a consistent, coerced state in the same
|
||||
/// <see cref="Changed"/> cycle.
|
||||
/// </summary>
|
||||
public void CoerceTheaterMode()
|
||||
{
|
||||
if (TheaterMode && !LavaEnabled && !WaveformEnabled)
|
||||
TheaterMode = false;
|
||||
}
|
||||
|
||||
/// <summary>Raise <see cref="Changed"/>. Called by the controls component after mutating a value.</summary>
|
||||
public void NotifyChanged() => Changed?.Invoke();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using DeepDrftPublic.Client.Services;
|
||||
|
||||
namespace DeepDrftTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the Theater-Mode auto-exit invariant on <see cref="WaveformVisualizerControlState"/>
|
||||
/// (Phase 20 bug fix): when both subsystems are disabled, <see cref="WaveformVisualizerControlState.CoerceTheaterMode"/>
|
||||
/// must force <c>TheaterMode = false</c> so observers never see a stranded-theater state.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user