Files
LlamaCasty/ytLive.Tests/ThemeResourceTests.cs
T
gramps 568586c541 fix(xaml): the app refused to launch — RecordPillToggle was defined before SegmentToggle
`BasedOn="{StaticResource SegmentToggle}"` resolves while the dictionary is being built, so a
forward reference fails at load with "Cannot find resource named 'SegmentToggle'". The style now
sits after SegmentToggle and SegmentLabel, where its base already exists.

Guard: ThemeResourceTests gains the ordering rule — within one dictionary, no StaticResource may
point at a key defined later in the same file. The existence check alone passed this, which is
exactly why the app died on launch while the tests were green.

Root cause of the unreliable test results this session: a stale `testhost` held ytLive.dll locked,
so builds silently kept the previous assembly (MSB3027 on copy). Killed it; the suite now rebuilds
and the two new facts actually run.
2026-10-30 10:29:22 -07:00

92 lines
4.4 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Xunit;
namespace ytLive.Tests;
/// <summary>A missing <c>{StaticResource}</c> key does not fail the build — WPF substitutes
/// <c>DependencyProperty.UnsetValue</c> and throws at the next layout pass. That is exactly how
/// the app crashed on 2026-10-30: the token conversion deleted <c>YtSliderKnobHoverBrush</c>
/// while two slider triggers still asked for it, so the first slider the creator touched
/// (the new socials opacity slider) killed the app inside <c>Ellipse.OnRender</c>.
///
/// <para>This guard scans every XAML file in the app and proves each referenced key exists —
/// either in the theme/App resources or in the same file — so the class cannot come back.</para></summary>
public class ThemeResourceTests
{
[Fact]
public void Every_StaticResource_reference_resolves_to_a_defined_key()
{
var root = System.IO.Path.GetDirectoryName(SourceAssert.RepoPath("App.xaml"))!;
var globalKeys = new HashSet<string>(StringComparer.Ordinal);
foreach (var file in new[] { System.IO.Path.Combine(root, "App.xaml") }
.Concat(Directory.GetFiles(System.IO.Path.Combine(root, "Themes"), "*.xaml")))
globalKeys.UnionWith(KeysIn(File.ReadAllText(file)));
var missing = new List<string>();
foreach (var path in XamlFiles())
{
var text = File.ReadAllText(path);
var localKeys = KeysIn(text);
foreach (Match match in Regex.Matches(text, @"\{StaticResource ([^}]+)\}"))
{
var key = match.Groups[1].Value.Trim();
if (key.StartsWith("{", StringComparison.Ordinal)) continue; // nested markup extension
if (globalKeys.Contains(key) || localKeys.Contains(key)) continue;
missing.Add($"{System.IO.Path.GetFileName(path)}: {key}");
}
}
Assert.True(missing.Count == 0,
"StaticResource keys with no definition (WPF throws UnsetValue at layout):\n "
+ string.Join("\n ", missing.Distinct()));
}
[Fact]
public void A_dictionary_never_references_a_key_defined_later_in_itself()
{
// StaticResource resolves while the dictionary is being built, so a forward reference
// fails at load with "Cannot find resource named …" — exactly how the app refused to
// launch on 2026-10-30 (a BasedOn pointing at a style defined further down the file).
var root = System.IO.Path.GetDirectoryName(SourceAssert.RepoPath("App.xaml"))!;
var files = new[] { System.IO.Path.Combine(root, "App.xaml") }
.Concat(Directory.GetFiles(System.IO.Path.Combine(root, "Themes"), "*.xaml"));
var forward = new List<string>();
foreach (var file in files)
{
var text = File.ReadAllText(file);
var definitions = Regex.Matches(text, @"x:Key=""([^""]+)""")
.ToDictionary(m => m.Groups[1].Value, m => m.Index, StringComparer.Ordinal);
foreach (Match use in Regex.Matches(text, @"\{StaticResource ([^}]+)\}"))
{
var key = use.Groups[1].Value.Trim();
if (!definitions.TryGetValue(key, out var defined)) continue;
if (defined > use.Index)
forward.Add($"{System.IO.Path.GetFileName(file)}: {key} used before it is defined");
}
}
Assert.True(forward.Count == 0,
"Forward StaticResource references fail at load:\n " + string.Join("\n ", forward.Distinct()));
}
private static HashSet<string> KeysIn(string text)
=> Regex.Matches(text, @"x:Key=""([^""]+)""").Select(m => m.Groups[1].Value)
.ToHashSet(StringComparer.Ordinal);
private static IEnumerable<string> XamlFiles()
{
var root = System.IO.Path.GetDirectoryName(SourceAssert.RepoPath("App.xaml"))!;
return Directory.EnumerateFiles(root, "*.xaml", SearchOption.AllDirectories)
.Where(p => !p.Contains($"{System.IO.Path.DirectorySeparatorChar}obj{System.IO.Path.DirectorySeparatorChar}", StringComparison.Ordinal)
&& !p.Contains($"{System.IO.Path.DirectorySeparatorChar}bin{System.IO.Path.DirectorySeparatorChar}", StringComparison.Ordinal)
&& !p.Contains("ytLive.Tests", StringComparison.Ordinal));
}
}