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()
|
private void ToggleLava()
|
||||||
{
|
{
|
||||||
ControlState.LavaEnabled = !ControlState.LavaEnabled;
|
ControlState.LavaEnabled = !ControlState.LavaEnabled;
|
||||||
|
ControlState.CoerceTheaterMode();
|
||||||
ControlState.NotifyChanged();
|
ControlState.NotifyChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleWaveform()
|
private void ToggleWaveform()
|
||||||
{
|
{
|
||||||
ControlState.WaveformEnabled = !ControlState.WaveformEnabled;
|
ControlState.WaveformEnabled = !ControlState.WaveformEnabled;
|
||||||
|
ControlState.CoerceTheaterMode();
|
||||||
ControlState.NotifyChanged();
|
ControlState.NotifyChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,19 @@ public sealed class WaveformVisualizerControlState
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action? Changed;
|
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>
|
/// <summary>Raise <see cref="Changed"/>. Called by the controls component after mutating a value.</summary>
|
||||||
public void NotifyChanged() => Changed?.Invoke();
|
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