UI 복잡 버전 234 RED→GREEN 완료(docs/UI_USECASES.md §A~§J): 라이브 테마 전환(DynamicResource 전환+ThemeResolver+HighContrast 테마, 재시작 없음), 디자인 토큰 체계·WCAG 대비 결함 5건 수정, 설정창 카테고리 내비/검색 재구성+AppConfig 40여 속성, 다운로드 관리자(속도/ETA/세그먼트맵/전체 일시정지-이어받기/CSV 내보내기/예약 게이트/중복 확인), 라이브러리·아카이브·작업공간·재생목록·즐겨찾기 편집 뷰, 접근성(자동화 속성 30여 개·커스텀 피어·docs/ACCESSIBILITY.md), 인터랙션(휠 클릭 새 탭·XButton 탐색·F12·영역 캡처·프로필 전환), 상태/알림(토스트·알림센터·온보딩·CrashReportDialog). 보강 테스트 35건+실창 E2E 4건(StaDispatcher 기반 — 프록시 설정이 기시작 핸들러를 건드리는 크래시 실결함 수정 포함) — 453/453 GREEN. 이전 미커밋 작업(CI 워크플로·Mdns·QueueHub·UpdateChecker·AGENTS/CLAUDE·빌드 스크립트) 일괄 포함
This commit is contained in:
parent
5a0ce730b2
commit
17f2f91152
88 changed files with 9463 additions and 1127 deletions
117
VideoDownloader.Tests/Infrastructure/SourceTree.cs
Normal file
117
VideoDownloader.Tests/Infrastructure/SourceTree.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using VideoDownloader.App;
|
||||
using VideoDownloader.Browser;
|
||||
using VideoDownloader.Core.Config;
|
||||
|
||||
namespace VideoDownloader.Tests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// UI 유즈케이스 RED 스펙 테스트(docs/UI_USECASES.md)용 소스 접근 헬퍼.
|
||||
/// 컴파일된 리소스가 아니라 저장소 원본 XAML·코드비하인드를 직접 읽어
|
||||
/// "UI 복잡 버전" 계약(리소스 토큰·요소·설정 속성 존재)을 검증한다.
|
||||
/// GREEN 전 단계이므로 대부분의 계약은 의도적으로 실패(RED)한다.
|
||||
/// </summary>
|
||||
public static class SourceTree
|
||||
{
|
||||
/// <summary>저장소 루트(VideoDownloader.slnx 위치). 테스트 bin 에서 위로 올라가 탐색.</summary>
|
||||
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, "VideoDownloader.slnx")))
|
||||
return dir.FullName;
|
||||
}
|
||||
throw new InvalidOperationException(
|
||||
$"솔루션 루트를 찾을 수 없습니다 (시작: {AppContext.BaseDirectory}). 빌드 산출물에서 실행 시 저장소 내 bin 디렉터리여야 합니다.");
|
||||
}
|
||||
|
||||
public static string AppPath(string relative) => Path.Combine(Root, "VideoDownloader.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<string> _mainWindowXaml = new(() => File.ReadAllText(AppPath("MainWindow.xaml")));
|
||||
private static readonly Lazy<string> _settingsXaml = new(() => File.ReadAllText(AppPath("SettingsWindow.xaml")));
|
||||
private static readonly Lazy<string> _sharedXaml = new(() => File.ReadAllText(AppPath("Themes/Shared.xaml")));
|
||||
private static readonly Lazy<string> _themeDark = new(() => File.ReadAllText(AppPath("Themes/Dark.xaml")));
|
||||
private static readonly Lazy<string> _themeLight = new(() => File.ReadAllText(AppPath("Themes/Light.xaml")));
|
||||
private static readonly Lazy<string> _mainWindowCs = new(() => File.ReadAllText(AppPath("MainWindow.xaml.cs")));
|
||||
private static readonly Lazy<string> _settingsCs = new(() => File.ReadAllText(AppPath("SettingsWindow.xaml.cs")));
|
||||
private static readonly Lazy<string> _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;
|
||||
|
||||
/// <summary>XAML 리소스 사전에 키(x:Key)가 정의되어 있는지.</summary>
|
||||
public static bool HasKey(string xaml, string key) => xaml.Contains($"x:Key=\"{key}\"");
|
||||
|
||||
/// <summary>SolidColorBrush 키의 색상(#RRGGBB) 조회. 없으면 null.</summary>
|
||||
public static string? BrushHex(string xaml, string key)
|
||||
{
|
||||
var m = Regex.Match(xaml, $"<SolidColorBrush\\s+x:Key=\"{Regex.Escape(key)}\"[^>]*Color=\"#([0-9A-Fa-f]{{6}})\"");
|
||||
return m.Success ? $"#{m.Groups[1].Value}" : null;
|
||||
}
|
||||
|
||||
/// <summary>WCAG 2.x 상대 휘도 기반 명도 대비율(1:1 ~ 21:1).</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Core·App·Browser 어셈블리에서 단순 이름으로 형식 탐색(아직 없는 신규 컴포넌트 계약용).</summary>
|
||||
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<Assembly> TargetAssemblies()
|
||||
{
|
||||
yield return typeof(AppConfig).Assembly;
|
||||
yield return typeof(MainWindow).Assembly;
|
||||
yield return typeof(TabArchive).Assembly;
|
||||
}
|
||||
|
||||
public static bool PropertyExists(Type type, string name) => type.GetProperty(name) is not null;
|
||||
|
||||
public static bool MethodExists(Type type, string name) => type.GetMethods().Any(m => m.Name == name);
|
||||
|
||||
/// <summary>유즈케이스 어설션 — 실패 메시지에 UC ID 와 사용자 스토리를 남긴다.</summary>
|
||||
public static void Usecase(bool ok, string id, string story) =>
|
||||
Assert.True(ok, $"{id} RED — {story}");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue