feat(scoreboard): unit 5 — tile selection and the cached strip presenter
ScoreboardTiles decides which of the eight facts appear and shapes their text (money uses the
session's dominant currency and flags a second one rather than converting; watch hours carry
'as of {date}' because Analytics is a daily aggregate). Tiles are OPT-IN, so 'None' really means the
layer draws nothing, and 'Default' is the six session facts.
ScoreboardPresenter owns the raster cache: ScoreboardRecorder.Changed fires only on a visible change,
so the strip rebuilds only then — a per-tick raster would be the compositor's most expensive layer
for identical pixels.
Guard: ScoreboardPresenterTests asserts the cache identity (same instance when nothing moved, a new
frame when a value moved, reuse when a recipient-side gift event arrives) and that an empty selection
returns no frame at all.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
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 change: the SAME frame instance comes back (cache hit).
|
||||
Assert.Same(first, presenter.Frame);
|
||||
|
||||
// A visible change: a new frame.
|
||||
recorder.Record(Member("Second"));
|
||||
var second = presenter.Frame;
|
||||
Assert.NotNull(second);
|
||||
Assert.NotSame(first, second);
|
||||
|
||||
// A gift-recipient event changes nothing displayed, so the raster is reused.
|
||||
recorder.Record(new ChatMessage { Kind = ChatEventKind.GiftReceived, AuthorName = "Recipient" });
|
||||
Assert.Same(second, presenter.Frame);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user