Files
LlamaCasty/ytLive.Tests/ScoreboardRendererTests.cs
T
gramps 882b8551d9 fix(scoreboard): the strip and the social bar now really share the frame
Reported: the ticker was painting OVER the socials. The coupling rule existed and was tested
(ScoreboardLayout.ResolveEdges) but nothing ever CALLED it — the strip's Y still came from its own
preference, so both bars landed on the south edge.

Now the resolution runs at publish time: the social bar keeps its side, the scoreboard takes the
other one, and its Y follows from that edge. The pair re-sorts whenever the social bar moves (the
preview click-toggle) or shows/hides, because the rule is "resolve from what is visible", not a
stored pairing.

The layer also gains an Edge preference (Top/Bottom) in its properties, labelled "socials take the
other" — a preference, never a claim on an occupied edge.

Also stabilised the opacity assertion: it samples a plate-only pixel 5px from the left edge instead
of the first semi-transparent pixel it finds (which could be anti-aliased text).
2026-10-30 15:11:35 -07:00

86 lines
4.4 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()
{
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;
}
}