Unit 7. The bay shows the track name (left-click toggles playback, right-click opens the picker — the label stays the control), a play/pause button, the duck badge, and a dB level. No ladder here, deliberately: nothing measures the music bed, and a decorative ladder would break the measured-not-painted rule. A test asserts the bay has no SegmentMeter. The level is real: `ITraxPlayer.UserGain` is a new creator gain multiplied onto the fixed design volume, separate from `LocalGain` (which follows the desktop mute). The VM's `TraxVolumeDb` is the same dB scale as the other faders, so unity means the bed's design level.
94 lines
3.9 KiB
C#
94 lines
3.9 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 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 */ }
|
|
}
|
|
});
|
|
}
|
|
}
|