using DeepDrftModels.DTOs; using DeepDrftPublic.Client.Services; using Microsoft.AspNetCore.Components; namespace DeepDrftTests; /// /// Unit tests for the play-queue orchestrator (). The queue is pure /// domain logic over the single-slot player, so it is exercised here against a recording fake /// () — no browser, no JS interop, no DI container. Coverage: /// enqueue, ordered advance, next/previous bounds, clear, current-index integrity, and /// auto-advance on the player's signal. /// [TestFixture] public class QueueServiceTests { private FakeStreamingPlayer _player = null!; private QueueService _queue = null!; [SetUp] public void SetUp() { _player = new FakeStreamingPlayer(); _queue = new QueueService(); _queue.Attach(_player); } [TearDown] public void TearDown() => _queue.Dispose(); private static List Tracks(int count) => Enumerable.Range(1, count) .Select(i => new TrackDto { EntryKey = $"track-{i}", TrackName = $"Track {i}", TrackNumber = i }) .ToList(); // --- Empty-queue invariants (no regression to single-track play) --- [Test] public void NewQueue_IsEmptyWithCurrentIndexNegativeOne() { Assert.Multiple(() => { Assert.That(_queue.Items, Is.Empty); Assert.That(_queue.CurrentIndex, Is.EqualTo(-1)); Assert.That(_queue.Current, Is.Null); Assert.That(_queue.HasNext, Is.False); Assert.That(_queue.HasPrevious, Is.False); }); } [Test] public async Task NextAndPrevious_OnEmptyQueue_AreNoOpsAndDriveNoPlayback() { await _queue.Next(); await _queue.Previous(); Assert.Multiple(() => { Assert.That(_queue.CurrentIndex, Is.EqualTo(-1)); Assert.That(_player.SelectedTracks, Is.Empty); }); } // --- PlayRelease: enqueue + ordered start --- [Test] public async Task PlayRelease_LoadsTracksInOrderAndStreamsFirst() { var tracks = Tracks(3); await _queue.PlayRelease(tracks); Assert.Multiple(() => { Assert.That(_queue.Items.Select(t => t.EntryKey), Is.EqualTo(new[] { "track-1", "track-2", "track-3" })); Assert.That(_queue.CurrentIndex, Is.EqualTo(0)); Assert.That(_queue.Current!.EntryKey, Is.EqualTo("track-1")); Assert.That(_player.SelectedTracks.Single().EntryKey, Is.EqualTo("track-1")); }); } [Test] public async Task PlayRelease_WithStartIndex_StartsMidAlbumAndKeepsRemainderQueued() { var tracks = Tracks(4); await _queue.PlayRelease(tracks, startIndex: 2); Assert.Multiple(() => { Assert.That(_queue.CurrentIndex, Is.EqualTo(2)); Assert.That(_queue.Current!.EntryKey, Is.EqualTo("track-3")); Assert.That(_player.SelectedTracks.Single().EntryKey, Is.EqualTo("track-3")); Assert.That(_queue.HasNext, Is.True); Assert.That(_queue.HasPrevious, Is.True); }); } [Test] public async Task PlayRelease_ClampsOutOfRangeStartIndex() { var tracks = Tracks(3); await _queue.PlayRelease(tracks, startIndex: 99); Assert.Multiple(() => { Assert.That(_queue.CurrentIndex, Is.EqualTo(2)); Assert.That(_player.SelectedTracks.Single().EntryKey, Is.EqualTo("track-3")); }); } [Test] public async Task PlayRelease_WithEmptyTracks_IsNoOp() { await _queue.PlayRelease(Enumerable.Empty()); Assert.Multiple(() => { Assert.That(_queue.Items, Is.Empty); Assert.That(_queue.CurrentIndex, Is.EqualTo(-1)); Assert.That(_player.SelectedTracks, Is.Empty); }); } [Test] public async Task PlayRelease_ReplacesAnExistingQueue() { await _queue.PlayRelease(Tracks(3)); var second = new List { new() { EntryKey = "x-1", TrackName = "X1" }, new() { EntryKey = "x-2", TrackName = "X2" }, }; await _queue.PlayRelease(second); Assert.Multiple(() => { Assert.That(_queue.Items, Has.Count.EqualTo(2)); Assert.That(_queue.Items.Select(t => t.EntryKey), Is.EqualTo(new[] { "x-1", "x-2" })); Assert.That(_queue.CurrentIndex, Is.EqualTo(0)); }); } // --- Next / Previous mechanics and bounds --- [Test] public async Task Next_AdvancesThroughTracksInOrder() { await _queue.PlayRelease(Tracks(3)); await _queue.Next(); Assert.That(_queue.Current!.EntryKey, Is.EqualTo("track-2")); await _queue.Next(); Assert.That(_queue.Current!.EntryKey, Is.EqualTo("track-3")); Assert.That(_player.SelectedTracks.Select(t => t.EntryKey), Is.EqualTo(new[] { "track-1", "track-2", "track-3" })); } [Test] public async Task Next_AtLastTrack_IsNoOp() { await _queue.PlayRelease(Tracks(2), startIndex: 1); await _queue.Next(); Assert.Multiple(() => { Assert.That(_queue.CurrentIndex, Is.EqualTo(1)); Assert.That(_queue.HasNext, Is.False); // Only the initial PlayRelease selection — Next at the end drove no further playback. Assert.That(_player.SelectedTracks, Has.Count.EqualTo(1)); }); } [Test] public async Task Previous_StepsBackThroughTracks() { await _queue.PlayRelease(Tracks(3), startIndex: 2); await _queue.Previous(); Assert.Multiple(() => { Assert.That(_queue.CurrentIndex, Is.EqualTo(1)); Assert.That(_queue.Current!.EntryKey, Is.EqualTo("track-2")); Assert.That(_player.SelectedTracks.Last().EntryKey, Is.EqualTo("track-2")); }); } [Test] public async Task Previous_AtFirstTrack_IsNoOp() { await _queue.PlayRelease(Tracks(3)); await _queue.Previous(); Assert.Multiple(() => { Assert.That(_queue.CurrentIndex, Is.EqualTo(0)); Assert.That(_queue.HasPrevious, Is.False); Assert.That(_player.SelectedTracks, Has.Count.EqualTo(1)); }); } // --- Enqueue / EnqueueRange --- [Test] public void Enqueue_AppendsWithoutChangingCurrentOrStartingPlayback() { _queue.Enqueue(new TrackDto { EntryKey = "a", TrackName = "A" }); Assert.Multiple(() => { Assert.That(_queue.Items, Has.Count.EqualTo(1)); Assert.That(_queue.CurrentIndex, Is.EqualTo(-1)); Assert.That(_player.SelectedTracks, Is.Empty); }); } [Test] public async Task Enqueue_AfterPlayRelease_ExtendsTheQueueAndEnablesHasNext() { await _queue.PlayRelease(Tracks(1)); Assert.That(_queue.HasNext, Is.False); _queue.Enqueue(new TrackDto { EntryKey = "appended", TrackName = "Appended" }); Assert.Multiple(() => { Assert.That(_queue.HasNext, Is.True); Assert.That(_queue.CurrentIndex, Is.EqualTo(0)); }); } [Test] public void EnqueueRange_AppendsAllTracks() { _queue.EnqueueRange(Tracks(3)); Assert.That(_queue.Items, Has.Count.EqualTo(3)); } // --- Clear --- [Test] public async Task Clear_EmptiesQueueAndResetsIndexWithoutStoppingPlayer() { await _queue.PlayRelease(Tracks(3)); _queue.Clear(); Assert.Multiple(() => { Assert.That(_queue.Items, Is.Empty); Assert.That(_queue.CurrentIndex, Is.EqualTo(-1)); Assert.That(_queue.Current, Is.Null); // Clear is a queue-state reset; it must not tear the player down. Assert.That(_player.StopCount, Is.EqualTo(0)); }); } // --- QueueChanged notifications --- [Test] public async Task MutatingOperations_RaiseQueueChanged() { var count = 0; _queue.QueueChanged += () => count++; await _queue.PlayRelease(Tracks(3)); // 1 await _queue.Next(); // 2 await _queue.Previous(); // 3 _queue.Enqueue(new TrackDto { EntryKey = "z", TrackName = "Z" }); // 4 _queue.Clear(); // 5 Assert.That(count, Is.EqualTo(5)); } [Test] public void Clear_OnAlreadyEmptyQueue_DoesNotRaiseQueueChanged() { var raised = false; _queue.QueueChanged += () => raised = true; _queue.Clear(); Assert.That(raised, Is.False); } // --- Auto-advance on TrackEnded --- [Test] public async Task TrackEnded_AutoAdvancesToNextTrack() { await _queue.PlayRelease(Tracks(3)); _player.RaiseTrackEnded(); Assert.Multiple(() => { Assert.That(_queue.CurrentIndex, Is.EqualTo(1)); Assert.That(_queue.Current!.EntryKey, Is.EqualTo("track-2")); Assert.That(_player.SelectedTracks.Last().EntryKey, Is.EqualTo("track-2")); }); } [Test] public async Task TrackEnded_OnLastTrack_DoesNotAdvanceOrReplay() { await _queue.PlayRelease(Tracks(2), startIndex: 1); _player.RaiseTrackEnded(); Assert.Multiple(() => { Assert.That(_queue.CurrentIndex, Is.EqualTo(1)); Assert.That(_player.SelectedTracks, Has.Count.EqualTo(1)); }); } [Test] public void TrackEnded_OnEmptyQueue_IsIgnored() { _player.RaiseTrackEnded(); Assert.Multiple(() => { Assert.That(_queue.CurrentIndex, Is.EqualTo(-1)); Assert.That(_player.SelectedTracks, Is.Empty); }); } [Test] public async Task TrackEnded_PlaysWholeAlbumThroughToTheEnd() { await _queue.PlayRelease(Tracks(3)); _player.RaiseTrackEnded(); // → track-2 _player.RaiseTrackEnded(); // → track-3 _player.RaiseTrackEnded(); // last track: no advance Assert.Multiple(() => { Assert.That(_queue.CurrentIndex, Is.EqualTo(2)); Assert.That(_player.SelectedTracks.Select(t => t.EntryKey), Is.EqualTo(new[] { "track-1", "track-2", "track-3" })); }); } // --- Attach lifecycle --- [Test] public async Task Dispose_UnsubscribesFromTrackEnded_SoNoAutoAdvanceAfterDispose() { await _queue.PlayRelease(Tracks(3)); _queue.Dispose(); _player.RaiseTrackEnded(); Assert.That(_queue.CurrentIndex, Is.EqualTo(0)); } [Test] public async Task Attach_ToNewPlayer_RedirectsPlaybackAndAutoAdvance() { var second = new FakeStreamingPlayer(); _queue.Attach(second); await _queue.PlayRelease(Tracks(3)); Assert.That(second.SelectedTracks.Single().EntryKey, Is.EqualTo("track-1")); // The old player's TrackEnded must no longer drive this queue. _player.RaiseTrackEnded(); Assert.That(_queue.CurrentIndex, Is.EqualTo(0)); // The newly attached player does. second.RaiseTrackEnded(); Assert.That(_queue.CurrentIndex, Is.EqualTo(1)); } /// /// Records the tracks the queue asks the player to stream and lets a test raise the /// player's organic end-of-stream signal. Implements the full /// surface but only the members the queue actually drives carry behavior; the rest are inert /// — the queue touches nothing else, which is exactly the seam this fake pins down. /// private sealed class FakeStreamingPlayer : IStreamingPlayerService { public List SelectedTracks { get; } = new(); public int StopCount { get; private set; } public void RaiseTrackEnded() => TrackEnded?.Invoke(); public Task SelectTrackStreaming(TrackDto track) { SelectedTracks.Add(track); CurrentTrack = track; return Task.CompletedTask; } public Task Stop() { StopCount++; return Task.CompletedTask; } public event Action? TrackEnded; // Part of the implemented contract but the queue never subscribes to it, so it is // intentionally never raised here. #pragma warning disable CS0067 public event Action? StateChanged; #pragma warning restore CS0067 // Inert remainder of the contract — the queue never invokes these. public bool IsInitialized => false; public bool IsLoaded => false; public bool IsLoading => false; public bool IsPlaying => false; public bool IsPaused => false; public double CurrentTime => 0; public double? Duration => null; public double Volume => 1.0; public double LoadProgress => 0; public string? ErrorMessage => null; public TrackDto? CurrentTrack { get; private set; } public double[]? WaveformProfile => null; public EventCallback? OnStateChanged { get; set; } public EventCallback? OnTrackSelected { get; set; } public bool IsStreamingMode => false; public bool CanStartStreaming => false; public bool HeaderParsed => false; public int BufferedChunks => 0; public Task InitializeAsync() => Task.CompletedTask; public Task SelectTrack(TrackDto track) => SelectTrackStreaming(track); public Task Unload() => Task.CompletedTask; public Task TogglePlayPause() => Task.CompletedTask; public Task Seek(double position) => Task.CompletedTask; public Task SetVolume(double volume) => Task.CompletedTask; public Task ClearError() => Task.CompletedTask; public Task WarmAudioContext() => Task.CompletedTask; public Task StageTrack(TrackDto track) => Task.CompletedTask; } }