using System.Reflection; using System.Text.RegularExpressions; using Paca.App; using Paca.Browser; using Paca.Core.Config; namespace Paca.Tests.Infrastructure; /// /// UI 유즈케이스 RED 스펙 테스트(docs/UI_USECASES.md)용 소스 접근 헬퍼. /// 컴파일된 리소스가 아니라 저장소 원본 XAML·코드비하인드를 직접 읽어 /// "UI 복잡 버전" 계약(리소스 토큰·요소·설정 속성 존재)을 검증한다. /// GREEN 전 단계이므로 대부분의 계약은 의도적으로 실패(RED)한다. /// public static class SourceTree { /// 저장소 루트(Paca.slnx 위치). 테스트 bin 에서 위로 올라가 탐색. public static string Root { get; } = FindRoot(); private static string FindRoot() { for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent) { if (File.Exists(Path.Combine(dir.FullName, "Paca.slnx")) || File.Exists(Path.Combine(dir.FullName, "VideoDownloader.slnx"))) return dir.FullName; } throw new InvalidOperationException( $"솔루션 루트를 찾을 수 없습니다 (시작: {AppContext.BaseDirectory}). 빌드 산출물에서 실행 시 저장소 내 bin 디렉터리여야 합니다."); } public static string AppPath(string relative) { var srcPath = Path.Combine(Root, "src", "Paca.App", relative); if (File.Exists(srcPath) || Directory.Exists(srcPath)) return srcPath; return Path.Combine(Root, "Paca.App", relative); } public static bool AppFileExists(string relative) => File.Exists(AppPath(relative)); public static bool DocFileExists(string fileName) => File.Exists(Path.Combine(Root, "docs", fileName)); private static readonly Lazy _mainWindowXaml = new(() => File.ReadAllText(AppPath("MainWindow.xaml"))); private static readonly Lazy _settingsXaml = new(() => File.ReadAllText(AppPath("SettingsWindow.xaml"))); private static readonly Lazy _sharedXaml = new(() => File.ReadAllText(AppPath("Themes/Shared.xaml"))); private static readonly Lazy _themeDark = new(() => File.ReadAllText(AppPath("Themes/Dark.xaml"))); private static readonly Lazy _themeLight = new(() => File.ReadAllText(AppPath("Themes/Light.xaml"))); private static readonly Lazy _mainWindowCs = new(() => File.ReadAllText(AppPath("MainWindow.xaml.cs"))); private static readonly Lazy _settingsCs = new(() => File.ReadAllText(AppPath("SettingsWindow.xaml.cs"))); private static readonly Lazy _readerModeCs = new(() => File.ReadAllText(AppPath("Browser/Features/ReaderMode.cs"))); public static string MainWindowXaml => _mainWindowXaml.Value; public static string SettingsWindowXaml => _settingsXaml.Value; public static string SharedXaml => _sharedXaml.Value; public static string ThemeDarkXaml => _themeDark.Value; public static string ThemeLightXaml => _themeLight.Value; public static string MainWindowCs => _mainWindowCs.Value; public static string SettingsWindowCs => _settingsCs.Value; public static string ReaderModeCs => _readerModeCs.Value; /// XAML 리소스 사전에 키(x:Key)가 정의되어 있는지. public static bool HasKey(string xaml, string key) => xaml.Contains($"x:Key=\"{key}\""); /// SolidColorBrush 키의 색상(#RRGGBB) 조회. 없으면 null. public static string? BrushHex(string xaml, string key) { var m = Regex.Match(xaml, $"]*Color=\"#([0-9A-Fa-f]{{6}})\""); return m.Success ? $"#{m.Groups[1].Value}" : null; } /// sys:Double 키의 숫자 값 조회. 없으면 null. public static double? GetDoubleResource(string xaml, string key) { var m = Regex.Match(xaml, $"([0-9.]+)"); return m.Success && double.TryParse(m.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture, out var val) ? val : null; } /// WCAG 2.x 상대 휘도 기반 명도 대비율(1:1 ~ 21:1). public static double ContrastRatio(string hexA, string hexB) { var l1 = RelativeLuminance(hexA); var l2 = RelativeLuminance(hexB); return (Math.Max(l1, l2) + 0.05) / (Math.Min(l1, l2) + 0.05); } private static double RelativeLuminance(string hex) { double Channel(int offset) => Convert.ToInt32(hex.Substring(offset, 2), 16) / 255.0; static double Linearize(double c) => c <= 0.03928 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4); var r = Linearize(Channel(1)); var g = Linearize(Channel(3)); var b = Linearize(Channel(5)); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } /// Core·App·Browser 어셈블리에서 단순 이름으로 형식 탐색(아직 없는 신규 컴포넌트 계약용). public static Type? FindType(string simpleName) { foreach (var asm in TargetAssemblies()) { Type[] types; try { types = asm.GetTypes(); } catch (ReflectionTypeLoadException ex) { // 일부 형식 로드 실패는 건너뛰고 로드된 형식만 대상으로 한다. types = ex.Types.Where(t => t is not null).Select(t => t!).ToArray(); } var found = types.FirstOrDefault(t => t.Name == simpleName); if (found is not null) return found; } return null; } private static IEnumerable TargetAssemblies() { yield return typeof(AppConfig).Assembly; yield return typeof(MainWindow).Assembly; yield return typeof(TabArchive).Assembly; } public static bool PropertyExists(Type type, string name) => UiContractPipeline.HasProperty(type, name); public static bool MethodExists(Type type, string name) => UiContractPipeline.HasMethod(type, name); /// 유즈케이스 어설션 — 실패 메시지에 UC ID 와 사용자 스토리를 남긴다. public static void Usecase(bool ok, string id, string story) => UiContractPipeline.AssertContract(ok, id, story); }