using System; using System.Collections.Generic; using System.Threading; using Xunit; using ytLive.Services; using ytLive.Services.Compositor; namespace ytLive.Tests; /// 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. public class ScoreboardRendererTests { private static readonly List 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(), 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 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; } }