Files
LlamaCasty/ytLive.Tests/ScoreboardRendererTests.cs
T
gramps c03acd0aa0 fix(scoreboard): the defaults were never applied — a record struct zero-initialises
Creator's defaults: white text, Background ON (black, 20%), speed capped at 200 px/s, text size shown
in real pixels, and "plate" dropped from the label.

The important find is underneath those: `ScoreboardStyle` is a record STRUCT, and `new
ScoreboardStyle()` zero-initialises — it does NOT run the constructor's parameter defaults. So the
shipped strip had null text colour, NO background and zero opacity, whatever the source said. That is
why the Background checkbox and the opacity slider looked like they had no code behind them: the
renderer was faithfully drawing the zeros it was handed.

An explicit parameterless constructor now carries the real defaults, and the renderer test asserts
them (white / on / black / 0.20 / max speed 200) so this class of silent-zero cannot return.

Readouts fixed too: the opacity and text-size setters never raised their own property, so the numbers
never moved while the render did. Text size now reads "24 px" instead of a bare slider value, and
snaps to whole pixels.
2026-10-30 15:24:14 -07:00

94 lines
4.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using Xunit;
using ytLive.Services;
using ytLive.Services.Compositor;
namespace ytLive.Tests;
/// <summary>The strip is state, not events: everything enabled is always on screen. These checks
/// paint the real frame, because a renderer that silently draws nothing would still pass a
/// structural test.</summary>
public class ScoreboardRendererTests
{
private static readonly List<string> Tiles = new() { "Members 12", "Milestones 3", "SC $47.00" };
[Fact]
public void The_strip_paints_the_plate_and_the_text_and_never_needs_a_scroll()
{
// The shipped defaults: white text, background ON, black, 20%.
var defaults = new ScoreboardStyle();
Assert.Equal("#FFFFFF", defaults.TextColor);
Assert.True(defaults.BackgroundEnabled);
Assert.Equal("#000000", defaults.BackgroundColor);
Assert.Equal(0.20, defaults.BackgroundOpacity, 3);
Assert.Equal(200, ytLive.Services.Compositor.ScoreboardPresenter.MaxPixelsPerSecond, 3);
Exception? failure = null;
var thread = new Thread(() =>
{
try
{
// Plate on: half-transparent blue behind white text.
var plated = ScoreboardRenderer.Render(Tiles, new ScoreboardStyle("#FFFFFF", true, "#0000FF", 0.5));
Assert.NotNull(plated);
Assert.Equal(ScoreboardRenderer.Width, plated!.Width);
Assert.True(plated.Height > 0);
Assert.True(Count(plated, (r, g, b, a) => b > 150 && r < 60 && g < 60 && a is > 100 and < 170) > 500,
"the plate must fill at the chosen colour and opacity");
Assert.True(Count(plated, (r, g, b, a) => r > 200 && g > 200 && b > 200 && a > 200) > 20,
"the text must be painted in the chosen colour");
// Plate off: the same area is transparent, the text survives.
var bare = ScoreboardRenderer.Render(Tiles, new ScoreboardStyle("#FFFFFF", false, "#0000FF", 0.5));
Assert.NotNull(bare);
Assert.Equal(0, Count(bare!, (r, g, b, a) => b > 150 && r < 60 && g < 60 && a > 100));
Assert.True(Count(bare!, (r, g, b, a) => r > 200 && g > 200 && b > 200 && a > 200) > 20);
// Text colour reaches the glyphs.
var red = ScoreboardRenderer.Render(Tiles, new ScoreboardStyle("#FF0000", false, "#000000", 0.5));
Assert.True(Count(red!, (r, g, b, a) => r > 180 && g < 80 && b < 80 && a > 200) > 20);
// The PLATE TOGGLE itself: same text, plate off vs on, different pixels. This is the
// check that answers "the checkbox has no code behind it".
var off = ScoreboardRenderer.Render(Tiles, new ScoreboardStyle("#FFFFFF", false, "#0000FF", 0.5));
var on = ScoreboardRenderer.Render(Tiles, new ScoreboardStyle("#FFFFFF", true, "#0000FF", 0.5));
Assert.True(Count(on!, (r, g, b, a) => b > 150 && r < 60 && g < 60 && a > 100)
> Count(off!, (r, g, b, a) => b > 150 && r < 60 && g < 60 && a > 100) + 500,
"turning the plate on must add plate pixels");
// Opacity: 20% and 80% must produce visibly different plate transparency.
var faint = ScoreboardRenderer.Render(Tiles, new ScoreboardStyle("#FFFFFF", true, "#0000FF", 0.2));
var solid = ScoreboardRenderer.Render(Tiles, new ScoreboardStyle("#FFFFFF", true, "#0000FF", 0.8));
// Sample a plate-only pixel (5px in from the left edge, vertical middle): the text is
// centred, so nothing anti-aliased can pollute the alpha reading.
static int AlphaOf(VideoFrame f) => f.BgraPixels[((f.Height / 2) * f.Width + 5) * 4 + 3];
Assert.InRange(AlphaOf(faint!), 35, 70); // ~20% of 255
Assert.InRange(AlphaOf(solid!), 190, 220); // ~80% of 255
Assert.True(AlphaOf(solid!) > AlphaOf(faint!) + 100, "the opacity slider must move the alpha");
// Nothing enabled: no frame at all (the layer draws nothing).
Assert.Null(ScoreboardRenderer.Render(Array.Empty<string>(), new ScoreboardStyle()));
}
catch (Exception ex) { failure = ex; }
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
Assert.Null(failure);
}
private static int Count(VideoFrame frame, Func<int, int, int, int, bool> predicate)
{
var hits = 0;
for (var i = 0; i < frame.BgraPixels.Length; i += 4)
if (predicate(frame.BgraPixels[i + 2], frame.BgraPixels[i + 1], frame.BgraPixels[i], frame.BgraPixels[i + 3]))
hits++;
return hits;
}
}