Files
LlamaCasty/ytLive.Tests/TraxToggleTests.cs
T
gramps a54149bd7c feat(dock): TRAX gets its own ladder — measured at the player's output
Creator: "why is there no meter for when trax is playing?"

Routing answer: TRAX rides the desktop loopback, so it already moved the GAME ladder — there was no
separate TRAX signal in the mix. But that ladder cannot answer the question a deaf creator actually
has: "is the music producing sound at all?" The play glyph cannot either — an open file with a dead
decoder still reports Playing.

So the player now measures its own output: \`MusicPlayer\` wraps the volume stage in an NAudio
\`MeteringSampleProvider\` and exposes \`Level\`; \`ITraxPlayer.Level\` keeps the fake honest. The VM maps
it through the same display scale (\`TraxMeterFraction\`), zeroed when not playing, and the TRAX bay
carries a 24-segment ladder above its fader.

This reverses my earlier "no ladder, nothing honest to show" — the honest thing arrived with a real
measurement. TraxBayTests flipped accordingly (it now asserts the ladder AND its source).
2026-10-30 13:22:13 -07:00

95 lines
4.0 KiB
C#

using System.IO;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Services.Audio;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>2026-10-09 bug: pressing TRAX started the music but the status light stayed off —
/// and the toggle couldn't pause, because the playing state was never refreshed after Play.
/// TRAX is a toggle: the light is green while playing, red with a terse scrolling message on
/// failure, gray when idle.</summary>
[Collection("RealApp")]
public sealed class TraxToggleTests
{
private sealed class FakePlayer : ITraxPlayer
{
public bool Starts = true;
public event Action<Exception?>? PlaybackEnded;
public string? TrackName { get; private set; }
public bool IsPlaying { get; private set; }
public float LocalGain { get; set; } = 1f;
public float UserGain { get; set; } = 1f;
public float Level { get; set; }
public void Load(string path) => TrackName = Path.GetFileNameWithoutExtension(path);
public void Play() => IsPlaying = Starts;
public void Pause() => IsPlaying = false;
public void Dispose() { }
public void Fail() { IsPlaying = false; PlaybackEnded?.Invoke(new IOException("device lost")); }
}
private readonly RealAppHost _app;
public TraxToggleTests(RealAppHost app) => _app = app;
[Fact]
public async Task Trax_Toggle_Lights_Green_While_Playing_And_Red_With_A_Terse_Error()
{
await _app.RunAsync(async () =>
{
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db");
var track = Path.Combine(Path.GetTempPath(), $"trax-{Guid.NewGuid():N}.mp3");
File.WriteAllBytes(track, new byte[16]);
var player = new FakePlayer();
MainViewModel.LayoutPathOverride = tempDb;
MainViewModel.TraxPlayerOverride = () => player;
var window = new MainWindow();
var vm = (MainViewModel)window.DataContext;
try
{
await vm.SetTraxTrackAsync(track, play: false);
Assert.False(vm.IsMusicPlaying);
var idle = vm.TraxDotBrush.Color;
vm.TraxLeftClick(); // press: plays → light on
Assert.True(vm.IsMusicPlaying);
var playing = vm.TraxDotBrush.Color;
Assert.NotEqual(idle, playing);
Assert.Null(vm.TraxError);
vm.TraxLeftClick(); // press again: pauses → light off
Assert.False(vm.IsMusicPlaying);
Assert.Equal(idle, vm.TraxDotBrush.Color);
player.Starts = false; // press: playback can't start
vm.TraxLeftClick();
Assert.False(vm.IsMusicPlaying);
Assert.NotNull(vm.TraxError);
Assert.True(vm.TraxError!.Length <= 32, $"error not terse: '{vm.TraxError}'");
var red = vm.TraxDotBrush.Color;
Assert.NotEqual(idle, red);
Assert.NotEqual(playing, red);
player.Starts = true; // recovery clears the error
vm.TraxLeftClick();
Assert.True(vm.IsMusicPlaying);
Assert.Null(vm.TraxError);
Assert.Equal(playing, vm.TraxDotBrush.Color);
player.Fail(); // device lost mid-play
Assert.False(vm.IsMusicPlaying);
Assert.Equal(red, vm.TraxDotBrush.Color);
}
finally
{
window.Close();
MainViewModel.LayoutPathOverride = null;
MainViewModel.TraxPlayerOverride = null;
SqliteConnection.ClearAllPools();
try { File.Delete(tempDb); File.Delete(track); } catch { /* best-effort cleanup */ }
}
});
}
}