using System.Text.RegularExpressions;
namespace VideoDownloader.Tests.Infrastructure;
///
/// 아이콘 글리프(PUA, Segoe Fluent Icons)를 본문 폰트 요소에 넣으면 tofu(□)로 렌더된다
/// (2026-08-18 실행 캡처로 확인 — "□ 모두 일시정지"). XAML/코드 정적 검사로 재발을 막는다.
/// 규칙:
/// ① PUA 를 쓰는 Content/Text 는 IconFont 보장 스타일(IconFont 를 설정하는 스타일 또는 그 BasedOn 파생)에
/// 있거나 요소에 FontFamily=IconFont 를 명시한다.
/// ② 한 문자열에 PUA 와 일반 텍스트를 섞지 않는다 — 폰트가 하나뿐이라 어느 한쪽은 반드시 깨진다.
///
public class IconGlyphXamlTests
{
private static readonly Regex PuaEntity = new(@"(E[0-9A-Fa-f]{3});", RegexOptions.Compiled);
/// Shared.xaml 에서 IconFont 를 보장하는 스타일 키 집합(BasedOn 파생 포함).
private static HashSet IconFontStyles()
{
var shared = SourceTree.SharedXaml;
var direct = new HashSet();
var basedOn = new Dictionary();
foreach (Match style in Regex.Matches(shared, "", RegexOptions.Singleline))
{
var key = style.Groups[1].Value;
if (style.Groups[3].Value.Contains("IconFont")) direct.Add(key);
var b = Regex.Match(style.Groups[2].Value, "BasedOn=\"\\{StaticResource (\\w+)\\}\"");
if (b.Success) basedOn[key] = b.Groups[1].Value;
}
// BasedOn 체인 전개 (파생이 부모의 FontFamily 세터를 상속)
bool grew;
do
{
grew = false;
foreach (var (child, parent) in basedOn)
if (!direct.Contains(child) && direct.Contains(parent)) { direct.Add(child); grew = true; }
} while (grew);
return direct;
}
private static IEnumerable Violations(string xaml, string fileName)
{
var iconStyles = IconFontStyles();
// 여는 태그 단위로 검사 — Button/TextBlock/Run 등 텍스트 표시 요소
foreach (Match tag in Regex.Matches(xaml, @"<(Button|TextBlock|Run|Label)\b[^>]*?>", RegexOptions.Singleline))
{
var open = tag.Value;
foreach (Match attr in Regex.Matches(open, "(?:Content|Text)=\"([^\"]*)\""))
{
var value = attr.Groups[1].Value;
if (!PuaEntity.IsMatch(value)) continue;
var line = xaml[..tag.Index].Count(c => c == '\n') + 1;
var stripped = PuaEntity.Replace(value, "").Trim();
if (stripped.Length > 0)
{
yield return $"{fileName} L{line}: PUA+일반 텍스트 혼합 \"{value}\" — 한 폰트로는 둘 다 못 그린다";
continue;
}
var styleMatch = Regex.Match(open, "Style=\"\\{(?:StaticResource|DynamicResource) (\\w+)\\}\"");
var styled = styleMatch.Success && iconStyles.Contains(styleMatch.Groups[1].Value);
var explicitFont = open.Contains("FontFamily=\"{DynamicResource IconFont}\"")
|| open.Contains("FontFamily=\"{StaticResource IconFont}\"");
if (!styled && !explicitFont)
yield return $"{fileName} L{line}: PUA 글리프 \"{value}\" 가 IconFont 보장 없이 사용됨(본문 폰트 → tofu)";
}
}
}
[Fact]
public void XAML_의_PUA_글리프는_IconFont_보장_하에서만_쓴다()
{
var violations = Violations(SourceTree.MainWindowXaml, "MainWindow.xaml")
.Concat(Violations(SourceTree.SettingsWindowXaml, "SettingsWindow.xaml"))
.ToList();
Assert.True(violations.Count == 0, "tofu 유발 지점:\n" + string.Join("\n", violations));
}
[Fact]
public void 코드비하인드도_PUA_와_일반_텍스트를_섞지_않는다()
{
// 예: LibraryViewToggle.Content = "; 그리드" — 본문 폰트 버튼이라 tofu + 잉여 세미콜론 노출
var offenders = Regex.Matches(SourceTree.MainWindowCs + File.ReadAllText(SourceTree.AppPath("MainWindow.Usecases.cs")),
@"""\\uE[0-9A-Fa-f]{3}[^""]*[가-힣A-Za-z][^""]*""")
.Select(m => m.Value).ToList();
Assert.True(offenders.Count == 0, "코드 내 혼합 문자열:\n" + string.Join("\n", offenders));
}
}