using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using Xunit; namespace ytLive.Tests; /// DESIGN.md became the visual standard on 2026-10-30. This is its guard: the document /// is the contract, `Themes/Tokens.xaml` is the implementation, and a token documented in one but /// missing (or different) in the other fails here — so the palette cannot drift into two truths /// the way the 2026-10-26 and 2026-10-29 Stitch exports did. /// /// Deliberately parses the markdown rather than duplicating a list: adding a colour to the /// standard requires implementing it, and changing a value in the theme without the document is a /// lie the gate catches. public class DesignTokensTests { private static readonly XNamespace Xaml = "http://schemas.microsoft.com/winfx/2006/xaml"; [Fact] public void Every_colour_in_the_standard_exists_as_a_token_brush_with_the_documented_value() { var documented = DocumentedColours(); Assert.True(documented.Count >= 20, $"parsed only {documented.Count} colour tokens out of DESIGN.md — the table format changed"); var brushes = Brushes(); foreach (var (token, hex) in documented) { var key = "Brush." + Pascal(token); Assert.True(brushes.ContainsKey(key), $"DESIGN.md documents `{token}` ({hex}) but Themes/Tokens.xaml has no `{key}` brush"); Assert.Equal(hex, brushes[key], ignoreCase: true); } } [Fact] public void The_meter_bands_are_the_standard_three() { var brushes = Brushes(); // DESIGN.md -> "Meter thresholds": nominal to -12 dB, caution to -3 dB, red above. Assert.Equal("#38bdf8", brushes["Brush.Meter.Nominal"], ignoreCase: true); Assert.Equal("#ffb95f", brushes["Brush.Meter.Caution"], ignoreCase: true); Assert.Equal("#ff5451", brushes["Brush.Meter.Peak"], ignoreCase: true); } [Fact] public void Tokens_merge_before_the_theme_so_styles_can_resolve_them() { var app = XDocument.Load(SourceAssert.RepoPath("App.xaml")); var sources = app.Descendants() .Where(e => e.Name.LocalName == "ResourceDictionary") .Select(e => (string?)e.Attribute("Source")) .Where(s => s != null) .ToList(); var tokens = sources.FindIndex(s => s!.EndsWith("Themes/Tokens.xaml", StringComparison.Ordinal)); var controls = sources.FindIndex(s => s!.EndsWith("Themes/Controls.xaml", StringComparison.Ordinal)); Assert.True(tokens >= 0, "App.xaml does not merge Themes/Tokens.xaml"); Assert.True(controls >= 0, "App.xaml no longer merges Themes/Controls.xaml"); Assert.True(tokens < controls, "Themes/Tokens.xaml must merge BEFORE Themes/Controls.xaml, or styles cannot resolve a token"); } [Fact] public void The_retired_brand_red_is_not_a_token() { // #e94560 was the general accent in 15 files. It is retired (DESIGN.md, 2026-10-30): // blue is interactive, red means live/record. Re-introducing it as a token would undo // the rule, so it must not appear anywhere in the token sheet. Assert.DoesNotContain("#e94560", File.ReadAllText(SourceAssert.RepoPath("Themes", "Tokens.xaml")), StringComparison.OrdinalIgnoreCase); } /// The `| Token | Hex |` tables under "## Colour", stopping at "## Typography". /// The "Rejected from the export" table lives further down and must not count. private static Dictionary DocumentedColours() { var result = new Dictionary(StringComparer.Ordinal); var inColour = false; foreach (var raw in File.ReadAllLines(SourceAssert.RepoPath("DESIGN.md"))) { var line = raw.Trim(); if (line.StartsWith("## ", StringComparison.Ordinal)) { inColour = line.StartsWith("## Colour", StringComparison.Ordinal); continue; } if (!inColour || !line.StartsWith("| `", StringComparison.Ordinal)) continue; var cells = line.Split('|').Select(c => c.Trim()).Where(c => c.Length > 0).ToArray(); if (cells.Length < 2) continue; // The token cell may carry a note after the backticked name("`tertiary` (electric blue)"). var token = Backticked(cells[0]); var hex = Backticked(cells[1]); if (!hex.StartsWith("#", StringComparison.Ordinal) || hex.Length != 7) continue; result[token] = hex; } return result; } private static Dictionary Brushes() { var dict = XDocument.Load(SourceAssert.RepoPath("Themes", "Tokens.xaml")); return dict.Descendants() .Where(e => e.Name.LocalName == "SolidColorBrush") .Select(e => ((string?)e.Attribute(Xaml + "Key"), (string?)e.Attribute("Color"))) .Where(p => p.Item1 != null && p.Item2 != null) .ToDictionary(p => p.Item1!, p => p.Item2!, StringComparer.Ordinal); } /// The first `backticked` run in a markdown cell, or the trimmed cell itself. private static string Backticked(string cell) { var start = cell.IndexOf('`'); if (start < 0) return cell.Trim(); var end = cell.IndexOf('`', start + 1); return end < 0 ? cell.Trim() : cell[(start + 1)..end].Trim(); } /// `surface-container-lowest` → `SurfaceContainerLowest`. private static string Pascal(string token) => string.Concat(token.Split('-', StringSplitOptions.RemoveEmptyEntries) .Select(part => char.ToUpperInvariant(part[0]) + part[1..])); }