using System.IO; using System.Reflection; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using Paca.App; using Paca.App.Browser.Features; using Paca.Server; using Paca.Tests.Infrastructure; using Xunit; namespace Paca.Tests.E2E; /// /// 초정밀 E2E 시각 & 디자인 게이트 검증 스위트 (Designpaca & Visual Quality Gate). /// 1. 데스크톱 XAML 테마 토큰 무결성 및 100% 키 일치도 검증 /// 2. WCAG 2.1 AA/AAA 기준 수학적 상대 휘도 명도 대비 검증 (TextPrimary, TextSecondary, TextMuted, Accent, Semantic) /// 3. 웹 플랫폼(site/) 디자인 시스템 토큰, 뷰포트 메타, 네비게이션 무결성 검증 /// 4. 내장 HTML(새 탭 NTP, 리더 모드, 웹 컴패니언) 렌더링 무결성 및 접근성 속성 검증 /// 5. 데스크톱 UI 창 및 공용 컨트롤 템플릿(버튼, 텍스트박스, 진행바, 콤보박스 등) 무결성 검증 /// public class VisualDesignGateTests { private static string SiteDir => Path.Combine(SourceTree.Root, "site"); // ========================================================================= // 1. 데스크톱 XAML 테마 토큰 무결성 & 100% 키 일치도 게이트 // ========================================================================= private static readonly string[] RequiredBrushKeys = [ "WindowBg", "WindowBgGradient", "PanelBg", "Surface2", "GlassBg", "GlassBorder", "CardBg", "CardBgHover", "DimOverlayBrush", "BorderBrush", "BorderSubtle", "TextPrimary", "TextSecondary", "TextMuted", "TextDisabled", "TrackBrush", "SuccessBrush", "ErrorBrush", "PendingBrush", "WarningBrush", "InfoBrush", "SelectionBrush", "FocusAdornerBrush", "LinkBrush", "VisitedLinkBrush", "AccentTextBrush", "AccentBrush", "AccentGradient", "AccentFadeBrush", "ScrollBarTrackBrush", "ScrollBarThumbBrush", "ScrollBarThumbHoverBrush", "ScrollBarThumbDragBrush", "Accent.Blue", "Accent.Green", "Accent.Pink", "Accent.Orange" ]; [Fact] public void Gate01_DarkTheme_AllRequiredTokensDefined() { var darkXaml = SourceTree.ThemeDarkXaml; foreach (var key in RequiredBrushKeys) { Assert.True(SourceTree.HasKey(darkXaml, key), $"[Dark Theme Gate] '{key}' 토큰이 Themes/Dark.xaml 에 누락되었습니다."); } } [Fact] public void Gate02_LightTheme_AllRequiredTokensDefined() { var lightXaml = SourceTree.ThemeLightXaml; foreach (var key in RequiredBrushKeys) { Assert.True(SourceTree.HasKey(lightXaml, key), $"[Light Theme Gate] '{key}' 토큰이 Themes/Light.xaml 에 누락되었습니다."); } } [Fact] public void Gate03_HighContrastTheme_AllRequiredTokensDefined() { var hcPath = SourceTree.AppPath("Themes/HighContrast.xaml"); Assert.True(File.Exists(hcPath), "Themes/HighContrast.xaml 파일이 존재해야 합니다."); var hcXaml = File.ReadAllText(hcPath); foreach (var key in RequiredBrushKeys) { Assert.True(SourceTree.HasKey(hcXaml, key), $"[HighContrast Theme Gate] '{key}' 토큰이 Themes/HighContrast.xaml 에 누락되었습니다."); } } // ========================================================================= // 2. WCAG 2.1 AA/AAA 수학적 상대 휘도 명도 대비 검증 게이트 // ========================================================================= [Fact] public void Gate04_DarkTheme_WcagContrast_MeetsAaAndAaa() { var dark = SourceTree.ThemeDarkXaml; var textPrimary = SourceTree.BrushHex(dark, "TextPrimary")!; var textSecondary = SourceTree.BrushHex(dark, "TextSecondary")!; var textMuted = SourceTree.BrushHex(dark, "TextMuted")!; var accentText = SourceTree.BrushHex(dark, "AccentTextBrush")!; var cardBg = SourceTree.BrushHex(dark, "CardBg")!; var windowBg = SourceTree.BrushHex(dark, "WindowBg")!; var panelBg = SourceTree.BrushHex(dark, "PanelBg")!; var cardHover = SourceTree.BrushHex(dark, "CardBgHover")!; // TextPrimary: AAA >= 7.0:1 Assert.True(SourceTree.ContrastRatio(textPrimary, cardBg) >= 7.0, $"Dark TextPrimary/CardBg ratio {SourceTree.ContrastRatio(textPrimary, cardBg):F2} < 7.0 (AAA)"); Assert.True(SourceTree.ContrastRatio(textPrimary, windowBg) >= 7.0, $"Dark TextPrimary/WindowBg ratio {SourceTree.ContrastRatio(textPrimary, windowBg):F2} < 7.0 (AAA)"); // TextSecondary: AA >= 4.5:1 Assert.True(SourceTree.ContrastRatio(textSecondary, cardBg) >= 4.5, $"Dark TextSecondary/CardBg ratio {SourceTree.ContrastRatio(textSecondary, cardBg):F2} < 4.5 (AA)"); Assert.True(SourceTree.ContrastRatio(textSecondary, panelBg) >= 4.5, $"Dark TextSecondary/PanelBg ratio {SourceTree.ContrastRatio(textSecondary, panelBg):F2} < 4.5 (AA)"); // TextMuted: AA >= 4.5:1 Assert.True(SourceTree.ContrastRatio(textMuted, cardBg) >= 4.5, $"Dark TextMuted/CardBg ratio {SourceTree.ContrastRatio(textMuted, cardBg):F2} < 4.5 (AA)"); Assert.True(SourceTree.ContrastRatio(textMuted, cardHover) >= 4.5, $"Dark TextMuted/CardBgHover ratio {SourceTree.ContrastRatio(textMuted, cardHover):F2} < 4.5 (AA)"); // AccentText: AA >= 4.5:1 Assert.True(SourceTree.ContrastRatio(accentText, cardBg) >= 4.5, $"Dark AccentText/CardBg ratio {SourceTree.ContrastRatio(accentText, cardBg):F2} < 4.5 (AA)"); } [Fact] public void Gate05_LightTheme_WcagContrast_MeetsAaAndAaa() { var light = SourceTree.ThemeLightXaml; var textPrimary = SourceTree.BrushHex(light, "TextPrimary")!; var textSecondary = SourceTree.BrushHex(light, "TextSecondary")!; var textMuted = SourceTree.BrushHex(light, "TextMuted")!; var accentText = SourceTree.BrushHex(light, "AccentTextBrush")!; var cardBg = SourceTree.BrushHex(light, "CardBg")!; var windowBg = SourceTree.BrushHex(light, "WindowBg")!; var panelBg = SourceTree.BrushHex(light, "PanelBg")!; var glassBg = SourceTree.BrushHex(light, "GlassBg")!; // TextPrimary: AAA >= 7.0:1 Assert.True(SourceTree.ContrastRatio(textPrimary, cardBg) >= 7.0, $"Light TextPrimary/CardBg ratio {SourceTree.ContrastRatio(textPrimary, cardBg):F2} < 7.0 (AAA)"); Assert.True(SourceTree.ContrastRatio(textPrimary, windowBg) >= 7.0, $"Light TextPrimary/WindowBg ratio {SourceTree.ContrastRatio(textPrimary, windowBg):F2} < 7.0 (AAA)"); // TextSecondary: AA >= 4.5:1 Assert.True(SourceTree.ContrastRatio(textSecondary, cardBg) >= 4.5, $"Light TextSecondary/CardBg ratio {SourceTree.ContrastRatio(textSecondary, cardBg):F2} < 4.5 (AA)"); Assert.True(SourceTree.ContrastRatio(textSecondary, panelBg) >= 4.5, $"Light TextSecondary/PanelBg ratio {SourceTree.ContrastRatio(textSecondary, panelBg):F2} < 4.5 (AA)"); // TextMuted: AA >= 4.5:1 Assert.True(SourceTree.ContrastRatio(textMuted, cardBg) >= 4.5, $"Light TextMuted/CardBg ratio {SourceTree.ContrastRatio(textMuted, cardBg):F2} < 4.5 (AA)"); Assert.True(SourceTree.ContrastRatio(textMuted, glassBg) >= 4.5, $"Light TextMuted/GlassBg ratio {SourceTree.ContrastRatio(textMuted, glassBg):F2} < 4.5 (AA)"); // AccentText: AA >= 4.5:1 Assert.True(SourceTree.ContrastRatio(accentText, cardBg) >= 4.5, $"Light AccentText/CardBg ratio {SourceTree.ContrastRatio(accentText, cardBg):F2} < 4.5 (AA)"); } [Fact] public void Gate06_HighContrastTheme_WcagContrast_ExceedsAaa() { var hcXaml = File.ReadAllText(SourceTree.AppPath("Themes/HighContrast.xaml")); var textPrimary = SourceTree.BrushHex(hcXaml, "TextPrimary")!; var textSecondary = SourceTree.BrushHex(hcXaml, "TextSecondary")!; var textMuted = SourceTree.BrushHex(hcXaml, "TextMuted")!; var cardBg = SourceTree.BrushHex(hcXaml, "CardBg")!; Assert.True(SourceTree.ContrastRatio(textPrimary, cardBg) >= 15.0, $"HighContrast TextPrimary/CardBg {SourceTree.ContrastRatio(textPrimary, cardBg):F2} < 15.0"); Assert.True(SourceTree.ContrastRatio(textSecondary, cardBg) >= 10.0, $"HighContrast TextSecondary/CardBg {SourceTree.ContrastRatio(textSecondary, cardBg):F2} < 10.0"); Assert.True(SourceTree.ContrastRatio(textMuted, cardBg) >= 7.0, $"HighContrast TextMuted/CardBg {SourceTree.ContrastRatio(textMuted, cardBg):F2} < 7.0"); } // ========================================================================= // 3. 웹 플랫폼(site/) 디자인 시스템 및 HTML 구조 무결성 게이트 // ========================================================================= [Fact] public void Gate07_WebSite_StylesCss_ContainsFullDesignTokens() { var cssPath = Path.Combine(SiteDir, "styles.css"); Assert.True(File.Exists(cssPath), "site/styles.css 파일이 존재해야 합니다."); var css = File.ReadAllText(cssPath); string[] requiredTokens = [ "--bg-canvas", "--surface-1", "--surface-2", "--surface-3", "--surface-4", "--border-subtle", "--border-medium", "--border-highlight", "--text-primary", "--text-secondary", "--text-muted", "--accent", "--accent-hover", "--accent-soft", "--accent-glow", "--success", "--warning", "--danger", "--step-0", "--step-1", "--step-2", "--step-3", "--step-4", "--step-5", "--space-1", "--space-2", "--space-3", "--space-4", "--space-5", "--radius-pill", "--radius-xl", "--radius-lg", "--radius-md", "--radius-sm", "--nav-h", "--container-max" ]; foreach (var tok in requiredTokens) { Assert.True(css.Contains(tok), $"[Web Token Gate] '{tok}' 변수가 site/styles.css 에 누락되었습니다."); } } [Theory] [InlineData("index.html")] [InlineData("guide.html")] [InlineData("releases.html")] [InlineData("404.html")] public void Gate08_WebSite_HtmlFiles_HaveProperMetaAndStructure(string fileName) { var filePath = Path.Combine(SiteDir, fileName); Assert.True(File.Exists(filePath), $"site/{fileName} 파일이 존재해야 합니다."); var html = File.ReadAllText(filePath); Assert.True(html.Contains("") || html.Contains(""), $"{fileName}에 DOCTYPE 선언이 누락되었습니다."); Assert.True(html.Contains("") || html.Contains(""), $"{fileName}에 UTF-8 메타 태그가 누락되었습니다."); Assert.True(html.Contains("name=\"viewport\""), $"{fileName}에 viewport 메타 태그가 누락되었습니다."); Assert.True(html.Contains("") && html.Contains(""), $"{fileName}에 title 태그가 누락되었습니다."); Assert.True(html.Contains("styles.css"), $"{fileName}에 styles.css 링크가 누락되었습니다."); } [Fact] public void Gate09_WebSite_ReleasesPage_HasMatchingContainerId() { var relHtml = File.ReadAllText(Path.Combine(SiteDir, "releases.html")); var siteJs = File.ReadAllText(Path.Combine(SiteDir, "site.js")); Assert.True(relHtml.Contains("id=\"release-root\""), "releases.html 에 dynamic renderer 용 id=\"release-root\" 가 존재해야 합니다."); Assert.True(siteJs.Contains("release-root"), "site.js 에 release-root 셀렉터 지원이 존재해야 합니다."); } [Fact] public void Gate10_WebSite_GuidePage_AllTocAnchorsMatch() { var guideHtml = File.ReadAllText(Path.Combine(SiteDir, "guide.html")); string[] requiredAnchors = ["install", "download", "batch", "phone", "shortcuts", "faq"]; foreach (var anchor in requiredAnchors) { Assert.True(guideHtml.Contains($"href=\"#{anchor}\""), $"guide.html TOC 에 href=\"#{anchor}\" 링크가 누락되었습니다."); Assert.True(guideHtml.Contains($"id=\"{anchor}\""), $"guide.html 본문에 id=\"{anchor}\" 대상 섹션이 누락되었습니다."); } } // ========================================================================= // 4. 내장 HTML(새 탭 NTP, 리더 모드, 웹 컴패니언) 렌더링 무결성 게이트 // ========================================================================= [Fact] public void Gate11_NewTabPageBuilder_RendersAccessibleResponsiveHtml() { var html = NewTabPageBuilder.BuildNtp(isDark: true); Assert.StartsWith("", html.TrimStart()); Assert.Contains("", html); Assert.Contains("name=\"viewport\"", html); Assert.Contains("class=\"dark\"", html); Assert.Contains("shortcuts-grid", html); Assert.Contains("search-box", html); Assert.Contains("customize-drawer", html); } [Fact] public void Gate12_ReaderModeBuilder_RendersValidKoreanDocument() { var html = ReaderMode.BuildReaderHtml("테스트 기사", "작성자", "요약문", "

본문 내용

", lightTheme: true); Assert.StartsWith("", html.TrimStart()); Assert.Contains("lang=\"ko\"", html); Assert.Contains("", html); Assert.Contains("name=\"viewport\"", html); Assert.Contains("테스트 기사", html); Assert.Contains("본문 내용", html); } [Fact] public void Gate13_WebCompanion_HasSafeViewportsAndGlassTokens() { var html = WebCompanion.Html; Assert.StartsWith("", html.TrimStart()); Assert.Contains("viewport-fit=cover", html); Assert.Contains("", html); Assert.Contains("--bg-gradient", html); Assert.Contains("--glass-border", html); Assert.Contains("PACA", html); } // ========================================================================= // 5. 데스크톱 UI 창 및 공용 컨트롤 템플릿 무결성 게이트 // ========================================================================= [Theory] [InlineData("MainWindow.xaml")] [InlineData("SettingsWindow.xaml")] [InlineData("BookmarkManagerWindow.xaml")] [InlineData("ExtensionManagerWindow.xaml")] [InlineData("LogViewerWindow.xaml")] [InlineData("CrashReportDialog.xaml")] public void Gate14_DesktopWindows_ExistAndUseUiFontAndWindowBg(string windowXamlFile) { var path = SourceTree.AppPath(windowXamlFile); Assert.True(File.Exists(path), $"{windowXamlFile} 파일이 존재해야 합니다."); var xaml = File.ReadAllText(path); Assert.True(xaml.Contains("FontFamily=\"{DynamicResource UiFont}\""), $"{windowXamlFile} 에 FontFamily=\"{{DynamicResource UiFont}}\" 선언이 필요합니다."); Assert.True(xaml.Contains("Background=\"{DynamicResource WindowBgGradient}\"") || xaml.Contains("Background=\"{DynamicResource WindowBg}\""), $"{windowXamlFile} 에 WindowBg/WindowBgGradient 배경 브러시 선언이 필요합니다."); } [Fact] public void Gate15_SharedStyles_AllCoreControlTemplatesDefined() { var shared = SourceTree.SharedXaml; string[] requiredStyles = [ "GhostButton", "PrimaryButton", "DarkTextBox", "AddressTextBox", "DarkCheckBox", "DarkRadioButton", "DarkComboBox", "AccentProgressBar", "FocusVisual", "SearchInputContainer" ]; foreach (var styleKey in requiredStyles) { Assert.True(SourceTree.HasKey(shared, styleKey), $"[Shared XAML Gate] '{styleKey}' 스타일이 Themes/Shared.xaml 에 누락되었습니다."); } } [Fact] public void Gate16_AddressPopup_AnchoredToAddressBarContainer_WithDynamicWidth() { var mwXaml = SourceTree.MainWindowXaml; Assert.True(mwXaml.Contains("PlacementTarget=\"{Binding ElementName=AddressBarContainer}\""), "AddressPopup 이 AddressBarContainer 에 앵커되어야 합니다."); Assert.True(mwXaml.Contains("Width=\"{Binding ActualWidth, ElementName=AddressBarContainer}\""), "AddressPopup 의 Width 가 AddressBarContainer 의 ActualWidth 에 동적으로 바인딩되어야 합니다."); } [Fact] public void Gate17_FlyoutsAndSubWindows_ProperlyStyled() { var bmPath = SourceTree.AppPath("BookmarkManagerWindow.xaml"); var extPath = SourceTree.AppPath("ExtensionManagerWindow.xaml"); var logPath = SourceTree.AppPath("LogViewerWindow.xaml"); var crashPath = SourceTree.AppPath("CrashReportDialog.xaml"); var bmXaml = File.ReadAllText(bmPath); var extXaml = File.ReadAllText(extPath); var logXaml = File.ReadAllText(logPath); var crashXaml = File.ReadAllText(crashPath); Assert.Contains("DynamicResource CardBgHover", bmXaml); Assert.Contains("DynamicResource AccentFadeBrush", bmXaml); Assert.Contains("DynamicResource AccentFadeBrush", extXaml); Assert.Contains("DynamicResource GlassBg", logXaml); Assert.Contains("DynamicResource ErrorBrush", crashXaml); } [Fact] public void Gate18_SettingsWindow_BentoLayoutAndCategories() { var settingsXaml = File.ReadAllText(SourceTree.AppPath("SettingsWindow.xaml")); string[] requiredSections = [ "SectionAppearance", "SectionToolbar", "SectionDownload", "SectionGeneral", "SectionBrowser", "SectionNotify", "SectionAccessibility", "SectionShortcuts", "SectionAbout" ]; foreach (var sec in requiredSections) { Assert.Contains($"x:Name=\"{sec}\"", settingsXaml); } Assert.Contains("x:Name=\"SettingsSearchBox\"", settingsXaml); Assert.Contains("x:Name=\"ResetDefaultsButton\"", settingsXaml); Assert.Contains("x:Name=\"ExportSettingsButton\"", settingsXaml); Assert.Contains("x:Name=\"ImportSettingsButton\"", settingsXaml); } [Fact] public void Gate19_DownloadCard_SegmentMapAndTypography() { var mwXaml = SourceTree.MainWindowXaml; Assert.Contains("DataType=\"{x:Type vm:DownloadItemViewModel}\"", mwXaml); Assert.Contains("x:Name=\"SegmentMapHost\"", mwXaml); Assert.Contains("Style=\"{StaticResource Type.Mono}\"", mwXaml); Assert.Contains("CornerRadius=\"{StaticResource CornerRadius.Card}\"", mwXaml); } [Fact] public void Gate20_TitleBarAndAddressBar_VerticalAlignmentAndSpacing() { var mwXaml = SourceTree.MainWindowXaml; Assert.Contains("x:Name=\"AddressBarContainer\"", mwXaml); Assert.Contains("x:Name=\"PageControlsBtn\"", mwXaml); Assert.Contains("x:Name=\"PageControlsDomainText\"", mwXaml); Assert.Contains("x:Name=\"BookmarkStarBtn\"", mwXaml); Assert.Contains("x:Name=\"NavProgress\"", mwXaml); } [Fact] public void Gate21_GlobalMediaHub_And_ProfileControls_Structure() { var mwXaml = SourceTree.MainWindowXaml; Assert.Contains("x:Name=\"GlobalMediaBtn\"", mwXaml); Assert.Contains("x:Name=\"ProfileBtn\"", mwXaml); Assert.Contains("x:Name=\"ProfileAvatarBadge\"", mwXaml); Assert.Contains("x:Name=\"ExtensionsBtn\"", mwXaml); } [Fact] public void Gate22_ToolbarPopups_RightAlignmentAndPureWhiteCanvas() { var mwXaml = SourceTree.MainWindowXaml; var mwCs = File.ReadAllText(SourceTree.AppPath("MainWindow.xaml.cs")); // Verify popups are dynamically aligned using CustomPopupPlacementCallback Assert.Contains("x:Name=\"ProfilePopup\"", mwXaml); Assert.Contains("x:Name=\"GlobalMediaPopup\"", mwXaml); Assert.Contains("x:Name=\"ExtensionsPopup\"", mwXaml); Assert.Contains("x:Name=\"MainMenuPopup\"", mwXaml); Assert.Contains("x:Name=\"PageControlsPopup\"", mwXaml); // Verify MainWindow.xaml.cs configures right alignment Assert.Contains("targetSize.Width - popupSize.Width", mwCs); Assert.Contains("ConfigureRight(ProfilePopup, ProfileBtn);", mwCs); Assert.Contains("ConfigureRight(MainMenuPopup, MainMenuBtn);", mwCs); Assert.Contains("ConfigureRight(GlobalMediaPopup, GlobalMediaBtn);", mwCs); Assert.Contains("ConfigureRight(ExtensionsPopup, ExtensionsBtn);", mwCs); // Verify default toolbar buttons do not include standalone settings (settings is in 3-dots main menu) Assert.DoesNotContain("\"settings\"", Paca.Core.Config.AppConfig.DefaultToolbarButtons); // Verify TabHost uses theme background (DynamicResource WindowBg) to prevent dark mode blinding glare Assert.Contains("Background=\"{DynamicResource WindowBg}\"", mwXaml); // Verify TabViewService initializes WebView2 with Color.White var tabViewServiceCode = File.ReadAllText(SourceTree.AppPath(Path.Combine("Services", "TabViewService.cs"))); Assert.Contains("DefaultBackgroundColor = System.Drawing.Color.White", tabViewServiceCode); } [Fact] public void Gate23_LuxuryDownloadExperience_UnifiedPopup_And_ThumbnailCard() { var mwXaml = SourceTree.MainWindowXaml; var mwCs = File.ReadAllText(SourceTree.AppPath("MainWindow.xaml.cs")); // 1. Download Card 16:9 Thumbnail, Glass Format Tag and Play Button Assert.Contains("Binding ThumbnailUrl", mwXaml); Assert.Contains("CardPlay_Click", mwXaml); Assert.Contains("CardPlay_Click", mwCs); // 2. Unified DownloadsPopup with Media Streams and Page Files Assert.Contains("PopupMediaDownloadsList", mwXaml); Assert.Contains("DownloadsList", mwXaml); Assert.Contains("OpenDownloadPanel_Click", mwXaml); Assert.Contains("OpenDownloadFolder_Click", mwXaml); Assert.Contains("OpenDownloadPanel_Click", mwCs); Assert.Contains("OpenDownloadFolder_Click", mwCs); // 3. Contextual Floating Batch Action Bar Assert.Contains("x:Name=\"BatchActionBar\"", mwXaml); Assert.Contains("x:Name=\"BatchSelectedCountText\"", mwXaml); Assert.Contains("UpdateBatchActionUi", mwCs); } [Fact] public void Gate24_UnifiedDockedSidePanel_Downloads_History_Bookmarks() { var mwXaml = SourceTree.MainWindowXaml; var mwCs = File.ReadAllText(SourceTree.AppPath("MainWindow.xaml.cs")); // 1. Right Docked Sidebar Segmented Tabs Assert.Contains("x:Name=\"SideTabsContainer\"", mwXaml); Assert.Contains("x:Name=\"SideTabDownloads\"", mwXaml); Assert.Contains("x:Name=\"SideTabHistory\"", mwXaml); Assert.Contains("x:Name=\"SideTabBookmarks\"", mwXaml); Assert.Contains("SideTab_Checked", mwXaml); // 2. 3 Multi-Views in Docked Sidebar Assert.Contains("x:Name=\"DownloadsPanelView\"", mwXaml); Assert.Contains("x:Name=\"HistoryPanelView\"", mwXaml); Assert.Contains("x:Name=\"BookmarksPanelView\"", mwXaml); // 3. Docked History & Bookmark Lists with Search and Grouping Assert.Contains("x:Name=\"HistorySearchBox\"", mwXaml); Assert.Contains("Name=\"HistoryList\"", mwXaml); Assert.Contains("x:Name=\"BookmarkSearchBox\"", mwXaml); Assert.Contains("Name=\"BookmarksList\"", mwXaml); // 4. Code-behind SidePanelTab and ShowSidePanel Routing Assert.Contains("enum SidePanelTab", mwCs); Assert.Contains("ShowSidePanel", mwCs); Assert.Contains("UpdateSidePanelBadge", mwCs); Assert.Contains("SideTab_Checked", mwCs); } // ========================================================================= // 6. 초고강도 디자인 & UX 무결성 감사 게이트 (Strict Design & Anti-Slop Gates) // ========================================================================= [Fact] public void Gate25_IconAndText_VerticalAlignment_StrictAudit() { var mwXaml = SourceTree.MainWindowXaml; var sharedXaml = SourceTree.SharedXaml; // 아이콘과 텍스트가 함께 들어가는 핵심 UI 블록(주소창 컨테이너, 탭 제목 등)에서 수직 정렬 Center 보장 Assert.Contains("x:Name=\"AddressBarContainer\"", mwXaml); Assert.Contains("Name=\"AddressBox\"", mwXaml); Assert.Contains("VerticalAlignment=\"Center\"", sharedXaml); // AddressTextBox 스타일에 VerticalContentAlignment="Center"가 설정되어 텍스트 수직 치우침이 없는지 확인 Assert.Contains("Property=\"VerticalContentAlignment\" Value=\"Center\"", sharedXaml); } [Fact] public void Gate26_InputControls_PaddingAndErgonomics_StrictAudit() { var mwXaml = SourceTree.MainWindowXaml; var sharedXaml = SourceTree.SharedXaml; // 인풋창 내부 텍스트가 경계선에 달라붙지 않도록 TextBox 기본 스타일 및 개별 인풋창에 Padding 설정 보장 Assert.Contains("TargetType=\"{x:Type TextBox}\"", sharedXaml); Assert.Contains("Property=\"Padding\"", sharedXaml); // 핵심 인풋창 존재 확인 Assert.Contains("Name=\"AddressBox\"", mwXaml); Assert.Contains("x:Name=\"HistorySearchBox\"", mwXaml); Assert.Contains("x:Name=\"BookmarkSearchBox\"", mwXaml); } [Fact] public void Gate27_ContainerOverflowAndTextTrimming_StrictAudit() { var mwXaml = SourceTree.MainWindowXaml; var sharedXaml = SourceTree.SharedXaml; // 가로 스크롤 및 텍스트 넘침 방지를 위한 CharacterEllipsis 적용 전수 검증 (스타일 Setter 및 인라인 어트리뷰트) Assert.Contains("Value=\"CharacterEllipsis\"", sharedXaml); Assert.Contains("TextTrimming=\"CharacterEllipsis\"", mwXaml); } [Fact] public void Gate28_InteractiveTargets_MinimumSizing_StrictAudit() { var mwXaml = SourceTree.MainWindowXaml; // 클릭 불가능하거나 너무 작은 터치 타깃 방지 (핵심 액션 버튼들이 적절한 크기를 갖추고 있는지 검증) Assert.Contains("x:Name=\"PageControlsBtn\"", mwXaml); Assert.Contains("x:Name=\"BookmarkStarBtn\"", mwXaml); Assert.Contains("x:Name=\"ExtensionsBtn\"", mwXaml); Assert.Contains("x:Name=\"MainMenuBtn\"", mwXaml); Assert.Contains("x:Name=\"GlobalMediaBtn\"", mwXaml); } [Fact] public void Gate29_Typography_ScaleAndHierarchy_StrictAudit() { var sharedXaml = SourceTree.SharedXaml; // 타이포그래피 토큰 계층: XLarge(22) > Large(16) > Base(13) > Small(11.5) var xlarge = SourceTree.GetDoubleResource(sharedXaml, "FontSize.XLarge"); var large = SourceTree.GetDoubleResource(sharedXaml, "FontSize.Large"); var baseSize = SourceTree.GetDoubleResource(sharedXaml, "FontSize.Base"); var small = SourceTree.GetDoubleResource(sharedXaml, "FontSize.Small"); Assert.NotNull(xlarge); Assert.NotNull(large); Assert.NotNull(baseSize); Assert.NotNull(small); Assert.True(xlarge > large, $"FontSize.XLarge ({xlarge}) > FontSize.Large ({large}) 계층구조 위반"); Assert.True(large > baseSize, $"FontSize.Large ({large}) > FontSize.Base ({baseSize}) 계층구조 위반"); Assert.True(baseSize > small, $"FontSize.Base ({baseSize}) > FontSize.Small ({small}) 계층구조 위반"); Assert.True(small >= 10.0, $"FontSize.Small ({small}) < 10.0 (안 보이는 폰트 버그)"); } [Fact] public void Gate30_AlignmentAndOffAxisBias_StrictAudit() { var mwXaml = SourceTree.MainWindowXaml; // 상단 바 및 주요 컨테이너의 구조적 균형 검증 (ToolbarRow, 네비게이션 진행바, 우측 도킹 패널 탭) Assert.Contains("x:Name=\"ToolbarRow\"", mwXaml); Assert.Contains("TitleBar_PreviewMouseLeftButtonDown", mwXaml); Assert.Contains("x:Name=\"SideTabsContainer\"", mwXaml); } [Fact] public void Gate31_AuxiliaryWindows_DesignSystemIntegrity_Audit() { string[] auxWindows = [ "SettingsWindow.xaml", "BookmarkManagerWindow.xaml", "CredentialVaultWindow.xaml", "ExtensionManagerWindow.xaml", "LogViewerWindow.xaml", "CrashReportDialog.xaml" ]; foreach (var winFile in auxWindows) { var winPath = SourceTree.AppPath(winFile); Assert.True(File.Exists(winPath), $"{winFile} 파일이 존재해야 합니다."); var xaml = File.ReadAllText(winPath); // 1. 공통 디자인 시스템 토큰 UiFont 사용 검증 (폰트 파편화 방지) Assert.True(xaml.Contains("FontFamily=\"{DynamicResource UiFont}\""), $"{winFile} 창은 FontFamily=\"{{DynamicResource UiFont}}\" 를 선언해야 합니다."); // 2. 다크/글래스 테마 배경 토큰 사용 검증 Assert.True(xaml.Contains("DynamicResource WindowBgGradient") || xaml.Contains("DynamicResource PanelBg") || xaml.Contains("DynamicResource GlassBg") || xaml.Contains("DynamicResource CardBg"), $"{winFile} 창은 표준 테마 배경 토큰을 사용해야 합니다."); } } [Fact] public void Gate32_SettingsSearchBox_Ergonomics_Audit() { var settingsXaml = SourceTree.SettingsWindowXaml; // SettingsSearchBox의 내부 패딩이 0이 아니라 최소 4px 이상의 인체공학적 여백을 가지는지 검증 var match = Regex.Match(settingsXaml, @"]*x:Name=""SettingsSearchBox""[^>]*Padding=""([^""]+)"""); Assert.True(match.Success, "SettingsSearchBox에 Padding 어트리뷰트가 정의되어야 합니다."); var padVal = match.Groups[1].Value; Assert.NotEqual("0", padVal.Trim()); } [Fact] public void Gate33_VerticalTabs_NewTabButtons_Visibility_StrictAudit() { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); sta.Invoke(() => { var window = new MainWindow(); window.Show(); // 1. 기본 가로 탭 모드일 때 NewTabBtn, NewTabMoreBtn은 Visible이어야 함 Assert.Equal(Visibility.Visible, window.NewTabBtn.Visibility); // 2. 세로 탭 모드로 전환 var toggleMethod = typeof(MainWindow).GetMethod("ToggleVerticalTabs", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(toggleMethod); toggleMethod.Invoke(window, null); // 3. 세로 탭 모드에서는 TabContainerPanel 내의 가로 전용 새 탭 버튼들이 숨겨져야(Collapsed) 시각적 붕괴 방지! Assert.Equal(Visibility.Collapsed, window.NewTabBtn.Visibility); Assert.Equal(Visibility.Collapsed, window.NewTabMoreBtn.Visibility); // 4. 다시 가로 탭 모드로 복귀 시 정상 복원 검증 toggleMethod.Invoke(window, null); Assert.Equal(Visibility.Visible, window.NewTabBtn.Visibility); Assert.Equal(Visibility.Visible, window.NewTabMoreBtn.Visibility); window.Close(); }); } [Fact] public void Gate34_StatusText_VerticalAlignment_StrictAudit() { var sharedXaml = SourceTree.SharedXaml; // StatusTextStyle에 VerticalAlignment = Center 세터가 정의되어 있어 상태바 텍스트의 상단 치우침/잘림을 방지하는지 검증 var match = Regex.Match(sharedXaml, @"]*x:Key=""StatusTextStyle""[^>]*>(.*?)", RegexOptions.Singleline); Assert.True(match.Success, "StatusTextStyle이 정의되어 있어야 합니다."); var styleBody = match.Groups[1].Value; Assert.True(styleBody.Contains("Property=\"VerticalAlignment\" Value=\"Center\""), "StatusTextStyle은 상태바 내 텍스트의 수직 정렬을 위해 VerticalAlignment=\"Center\"를 선언해야 합니다."); } [Fact] public void Gate35_VerticalTabsHeader_InteractiveTargets_Audit() { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); sta.Invoke(() => { var window = new MainWindow(); var method = typeof(MainWindow).GetMethod("CreateVerticalTabsHeader", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(method); var header = (Border)method.Invoke(window, null)!; Assert.NotNull(header); // 헤더 내부의 모든 Button을 탐색하여 최소 클릭 타깃 높이(>= 30px) 검증 var buttons = FindLogicalChildren