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, @"", 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