Files
LlamaCasty/ytLive.Tests/ScoreboardPresenterTests.cs
T
gramps 28525c90aa feat(scoreboard): scroll speed is a property (20-400 px/s, persisted)
The marquee now has the dial it needs. `ScoreboardPresenter.PixelsPerSecond` is the single owner of
the range (20-400, default 120), the layer's properties expose it with a live px/s readout, and the
value is persisted as Scoreboard.ScrollSpeed because it is a READABILITY choice — re-dialling it
every launch would be a tax on the creator who needs the strip to be readable.

Advance() still takes an optional override so tests can drive exact distances.

Guard: the presenter test proves speed is real (same elapsed time, twice the speed, different
window) — not a value that is stored and ignored — and the panel test pins the control, its range
and the persistence key.
2026-10-30 15:19:28 -07:00

84 lines
3.5 KiB
C#

using System.Linq;
using System.Threading;
using Xunit;
using ytLive.Models;
using ytLive.Services.Compositor;
namespace ytLive.Tests;
/// <summary>The strip's cache is a performance rule with teeth: it must rebuild when a displayed
/// value moves and must NOT rebuild otherwise, or the compositor pays for a raster per tick into
/// pixels that did not change.</summary>
public class ScoreboardPresenterTests
{
private static ChatMessage Member(string name) => new() { Kind = ChatEventKind.NewMember, AuthorName = name };
[Fact]
public void The_strip_rebuilds_only_when_a_displayed_value_moves()
{
System.Exception? failure = null;
var thread = new Thread(() =>
{
try
{
var recorder = new ScoreboardRecorder();
var tiles = new ScoreboardTiles
{
SuperChats = false, WatchHours = false, Subscribers = false,
NewMembers = true, Milestones = false, LastMember = true,
LastSuperChat = false, LastGift = false,
};
var presenter = new ScoreboardPresenter(recorder, new ScoreboardStyle());
presenter.SetTiles(tiles);
// Before a session: nothing counted, nothing to draw.
Assert.Null(presenter.Frame);
recorder.StartSession();
recorder.Record(Member("First"));
var first = presenter.Frame;
Assert.NotNull(first);
// No tick, no change: the same pixels come back (the run is cached).
Assert.Equal(first!.BgraPixels, presenter.Frame!.BgraPixels);
// A TICK moves the window: the pixels must differ.
presenter.Advance(0.5);
var ticked = presenter.Frame;
Assert.NotNull(ticked);
Assert.NotEqual(first.BgraPixels, ticked!.BgraPixels);
// SPEED is a real dial: the same elapsed time at twice the speed moves twice as far,
// so the window lands somewhere else.
var slow = new ScoreboardPresenter(recorder, new ScoreboardStyle()) { PixelsPerSecond = 60 };
var fast = new ScoreboardPresenter(recorder, new ScoreboardStyle()) { PixelsPerSecond = 120 };
slow.SetTiles(tiles);
fast.SetTiles(tiles);
slow.Advance(1.0);
fast.Advance(1.0);
Assert.NotEqual(slow.Frame!.BgraPixels, fast.Frame!.BgraPixels);
// A visible change re-rasterises the run.
recorder.Record(Member("Second"));
var second = presenter.Frame;
Assert.NotNull(second);
Assert.NotEqual(ticked.BgraPixels, second!.BgraPixels);
// A gift-recipient event changes nothing displayed: same window, same pixels.
var beforeGift = presenter.Frame!.BgraPixels.ToArray();
recorder.Record(new ChatMessage { Kind = ChatEventKind.GiftReceived, AuthorName = "Recipient" });
Assert.Equal(beforeGift, presenter.Frame!.BgraPixels);
// Disabling every tile returns no frame, and the layer draws nothing.
presenter.SetTiles(ScoreboardTiles.None);
Assert.Null(presenter.Frame);
}
catch (System.Exception ex) { failure = ex; }
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
Assert.Null(failure);
}
}