Add whole-release embeds to FramePlayer and un-gate the release embed share affordance

The queue gains an armed-but-idle state (Arm/Start) so a release embed stages track 0 prerender-safe, then queues the full release on first play and auto-advances.
This commit is contained in:
daniel-c-harvey
2026-06-19 12:05:35 -04:00
parent 1931574ad4
commit 912256d99a
12 changed files with 560 additions and 47 deletions
+121
View File
@@ -144,6 +144,127 @@ public class QueueServiceTests
});
}
// --- Arm: prerender-safe load without streaming (release embed) ---
[Test]
public void Arm_LoadsTracksAtIndexZero_WithoutStreaming()
{
var tracks = Tracks(3);
_queue.Arm(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.IsArmed, Is.True);
// Arming is interop-free state only: nothing must have been streamed yet.
Assert.That(_player.SelectedTracks, Is.Empty);
});
}
[Test]
public void Arm_WithSingleTrackRelease_ArmsAOneItemQueueWithoutError()
{
_queue.Arm(Tracks(1));
Assert.Multiple(() =>
{
Assert.That(_queue.Items, Has.Count.EqualTo(1));
Assert.That(_queue.CurrentIndex, Is.EqualTo(0));
Assert.That(_queue.IsArmed, Is.True);
Assert.That(_queue.HasNext, Is.False);
Assert.That(_player.SelectedTracks, Is.Empty);
});
}
[Test]
public void Arm_WithEmptyTracks_IsNoOpAndLeavesQueueDisarmed()
{
_queue.Arm(Enumerable.Empty<TrackDto>());
Assert.Multiple(() =>
{
Assert.That(_queue.Items, Is.Empty);
Assert.That(_queue.CurrentIndex, Is.EqualTo(-1));
Assert.That(_queue.IsArmed, Is.False);
});
}
[Test]
public async Task Start_OnArmedQueue_StreamsTrackZeroAndDisarms()
{
// Models the embed's first-gesture path: FramePlayer arms the queue (no stream), then the
// play click routes through Start().
_queue.Arm(Tracks(3));
await _queue.Start();
Assert.Multiple(() =>
{
Assert.That(_queue.CurrentIndex, Is.EqualTo(0));
Assert.That(_queue.IsArmed, Is.False);
Assert.That(_player.SelectedTracks.Single().EntryKey, Is.EqualTo("track-1"));
});
}
[Test]
public async Task Start_OnUnarmedQueue_IsNoOp()
{
// A non-embed flow (PlayRelease already streaming) must not be disturbed by a stray Start.
await _queue.PlayRelease(Tracks(3));
var streamedBefore = _player.SelectedTracks.Count;
await _queue.Start();
Assert.Multiple(() =>
{
Assert.That(_queue.IsArmed, Is.False);
Assert.That(_player.SelectedTracks, Has.Count.EqualTo(streamedBefore),
"Start on an unarmed queue must not re-stream");
});
}
[Test]
public async Task ArmedQueue_StartedThenAdvancesThroughWholeReleaseOnTrackEnded()
{
_queue.Arm(Tracks(3));
await _queue.Start();
_player.RaiseTrackEnded(); // → track-2
_player.RaiseTrackEnded(); // → track-3
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" }));
});
}
[Test]
public void Arm_RaisesQueueChanged()
{
var raised = false;
_queue.QueueChanged += () => raised = true;
_queue.Arm(Tracks(2));
Assert.That(raised, Is.True);
}
[Test]
public async Task Clear_DisarmsAnArmedQueue()
{
_queue.Arm(Tracks(2));
_queue.Clear();
Assert.That(_queue.IsArmed, Is.False);
await Task.CompletedTask;
}
// --- Next / Previous mechanics and bounds ---
[Test]