Files
LlamaCasty/ytLive.Tests/OutputPaletteTests.cs
T
gramps 7adff685c0 design: the on-stream graphics join the standard (OutputPalette)
The broadcast picture is part of the app, so it stops carrying its own colours. Alert cards, the
ticker and the branding flash now paint from OutputPalette, which Mirrors DESIGN.md for the brand
tokens and owns the alert box's six-event accent language (gold/violet/pink — content, not
chrome; the standard has no purple and should not grow one, but they live in ONE file instead of
57 scattered literals).

It is a separate type because the renderers run on the frame-pump thread and must not touch
application resources. The duplication is made safe by OutputPaletteTests: it parses
Themes/Tokens.xaml and fails if any mirrored token drifts, and fails if a renderer reintroduces a
colour literal or "Segoe UI".

Ticker and flash glyphs now use the bundled Inter instead of Segoe UI. Neon red is the standard
live red — one red in the product, including the credit.

Fixed on the way: the palette's font fields were static pack-URI initialisers, which throw
TypeInitializationException in any host without a WPF Application (the test process caught it);
they are lazy now with a name fallback.
2026-10-30 09:30:56 -07:00

82 lines
3.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Media;
using Xunit;
using ytLive.Services.Compositor;
namespace ytLive.Tests;
/// <summary>On-stream graphics cannot read application resources (the frame pump is not the UI
/// thread), so their colours live in <see cref="OutputPalette"/>. That duplication is safe only
/// if it is checked: this test proves every brand token there equals the brush in
/// <c>Themes/Tokens.xaml</c>, and that the renderers carry no colour literals of their own.
///
/// <para>Without it, the broadcast picture would drift away from the UI one "quick fix" at a
/// time — the exact failure mode DESIGN.md exists to stop.</para></summary>
public class OutputPaletteTests
{
[Fact]
public void The_on_stream_palette_matches_the_theme_and_the_renderers_carry_no_literals()
{
var brushes = Tokens();
var mirrored = new (string Brush, Color Colour)[]
{
("Brush.Primary", OutputPalette.Live),
("Brush.PrimaryContainer", OutputPalette.LiveDark),
("Brush.Secondary", OutputPalette.Amber),
("Brush.Attention", OutputPalette.Attention),
("Brush.Positive", OutputPalette.Positive),
("Brush.Error", OutputPalette.Error),
("Brush.ErrorContainer", OutputPalette.ErrorSurface),
("Brush.Tertiary", OutputPalette.Tertiary),
("Brush.OnSurface", OutputPalette.TextPrimary),
("Brush.OnSurfaceVariant", OutputPalette.TextMuted),
("Brush.Background", OutputPalette.Background),
("Brush.SurfaceContainer", OutputPalette.Surface),
("Brush.SurfaceContainerHigh", OutputPalette.SurfaceHigh),
("Brush.Outline", OutputPalette.Outline),
};
foreach (var (brushKey, colour) in mirrored)
{
Assert.True(brushes.ContainsKey(brushKey), $"{brushKey} missing from Themes/Tokens.xaml");
Assert.True(brushes[brushKey] == colour,
$"{brushKey} is {brushes[brushKey]} in the theme but {colour} in OutputPalette — the " +
"broadcast picture and the UI have drifted apart");
}
// The renderers must not introduce colours of their own; the alert accents belong to
// OutputPalette too, so one file owns every on-stream value.
foreach (var file in new[]
{
"Services/AlertRenderer.cs", "Services/Compositor/AlertTickerRenderer.cs",
"Services/Compositor/BrandFlashPresenter.cs",
})
{
var text = SourceAssert.Text(file.Split('/'));
Assert.DoesNotContain("Color.FromRgb(0x", text);
Assert.DoesNotContain("\"Segoe UI\"", text);
}
}
private static Dictionary<string, Color> Tokens()
{
var text = SourceAssert.Text("Themes", "Tokens.xaml");
var result = new Dictionary<string, Color>(StringComparer.Ordinal);
foreach (Match m in Regex.Matches(text, "x:Key=\"(Brush\\.[^\"]+)\"\\s+Color=\"#([0-9a-fA-F]{6,8})\""))
{
var hex = m.Groups[2].Value;
if (hex.Length == 8) hex = hex[2..]; // drop the alpha the palette does not mirror
var r = int.Parse(hex[..2], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var g = int.Parse(hex[2..4], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var b = int.Parse(hex[4..6], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
result[m.Groups[1].Value] = Color.FromRgb((byte)r, (byte)g, (byte)b);
}
return result;
}
}