using System.Text.RegularExpressions; namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests; public sealed class LegacyWorkspaceLayoutContractTests { private static readonly string RepositoryRoot = FindRepositoryRoot(); private static readonly string WebRoot = Path.Combine( RepositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web"); private static readonly string Markup = File.ReadAllText(Path.Combine(WebRoot, "index.html")); private static readonly string Styles = File.ReadAllText(Path.Combine(WebRoot, "styles.css")); private static readonly string App = File.ReadAllText(Path.Combine(WebRoot, "app.js")); private static readonly string Navigation = File.ReadAllText( Path.Combine(WebRoot, "workspace-navigation.js")); [Fact] public void ShellUsesSidebarScreensBesidePersistentSchedule() { var workspaceStart = Markup.IndexOf("id=\"workspace-region\"", StringComparison.Ordinal); var navigationStart = Markup.IndexOf("id=\"workspace-navigation\"", StringComparison.Ordinal); var marketStart = Markup.IndexOf("id=\"market-tabs\"", StringComparison.Ordinal); var settingsTabStart = Markup.IndexOf("id=\"workspace-settings-tab\"", StringComparison.Ordinal); var stockStart = Markup.IndexOf("id=\"stock-workspace\"", StringComparison.Ordinal); var catalogStart = Markup.IndexOf("id=\"catalog-workspace\"", StringComparison.Ordinal); var settingsStart = Markup.IndexOf("id=\"settings-workspace\"", StringComparison.Ordinal); var scheduleStart = Markup.IndexOf("class=\"playlist-panel\"", StringComparison.Ordinal); Assert.True(workspaceStart >= 0); Assert.True(navigationStart > workspaceStart); Assert.True(marketStart > navigationStart); Assert.True(settingsTabStart > marketStart); Assert.True(stockStart > settingsTabStart); Assert.True(catalogStart > stockStart); Assert.True(settingsStart > catalogStart); Assert.True(scheduleStart > settingsStart); Assert.Matches( @"<\/section>\s*<\/div>\s*<\/section>\s*<\/section>\s*
]*hidden inert", Markup); Assert.Matches("id=\"settings-workspace\"[^>]*hidden inert", Markup); Assert.Contains("항상 표시

송출 스케줄

", Markup, StringComparison.Ordinal); } [Fact] public void SidebarOwnsEachNativeMarketButtonAndKeepsSettingsOutsideTheirOrder() { var marketStart = Markup.IndexOf("
", marketStart, StringComparison.Ordinal); Assert.True(marketStart >= 0 && marketEnd > marketStart); var marketMarkup = Markup[marketStart..marketEnd]; var expected = new[] { "overseas", "exchange", "index", "kospiIndustry", "kosdaqIndustry", "comparison", "theme", "overseasStocks", "expert", "tradingHalt" }; var actual = Regex.Matches(marketMarkup, "data-tab-id=\"([^\"]+)\"") .Select(match => match.Groups[1].Value) .ToArray(); Assert.Equal(expected, actual); Assert.Equal(expected.Length, Regex.Matches(marketMarkup, "]*role=\"navigation\"[^>]*aria-label=", Markup); Assert.Matches( "id=\"workspace-nav-toggle\"[^>]*aria-expanded=\"true\"[\\s\\S]*?" + "aria-controls=\"workspace-nav-items workspace-nav-footer\"", Markup); Assert.Matches( "id=\"workspace-nav-items\"[^>]*role=\"group\"[^>]*aria-label=", Markup); Assert.Matches( "id=\"market-tabs\"[^>]*role=\"group\"[^>]*aria-label=", Markup); Assert.Contains( "id=\"workspace-nav-footer\" class=\"workspace-nav-footer\"", Markup, StringComparison.Ordinal); } [Fact] public void ModernOperatorThemePreservesAccessibleAndSemanticStates() { Assert.Contains( "Modern operator theme: visual-only overrides; geometry and native contracts stay intact", Styles, StringComparison.Ordinal); Assert.Contains("--ui-canvas:", Styles, StringComparison.Ordinal); Assert.Contains("--ui-accent:", Styles, StringComparison.Ordinal); Assert.Contains("--ui-success:", Styles, StringComparison.Ordinal); Assert.Contains("--ui-danger:", Styles, StringComparison.Ordinal); Assert.Contains( "font-family: \"Segoe UI Variable\", \"Segoe UI\", \"Noto Sans KR\"", Styles, StringComparison.Ordinal); Assert.Matches( new Regex( @"button:focus-visible,[\s\S]*?outline:\s*2px solid var\(--ui-accent\)", RegexOptions.CultureInvariant), Styles); Assert.Contains(".playlist-data-row.on-air.selected", Styles, StringComparison.Ordinal); Assert.Contains(".playout-actions .take-in:not(:disabled)", Styles, StringComparison.Ordinal); Assert.Contains(".playout-actions .next:not(:disabled)", Styles, StringComparison.Ordinal); Assert.Contains(".playout-actions .take-out:not(:disabled)", Styles, StringComparison.Ordinal); Assert.Contains( ".playlist-data-row[data-active=\"true\"]:not(.on-air)", Styles, StringComparison.Ordinal); Assert.Contains( ".playlist-data-row[aria-selected=\"true\"]:not(.on-air)", Styles, StringComparison.Ordinal); Assert.Contains("@media (forced-colors: active)", Styles, StringComparison.Ordinal); Assert.Contains("scrollbar-gutter: stable", Styles, StringComparison.Ordinal); Assert.Contains("@media (prefers-reduced-motion: reduce)", Styles, StringComparison.Ordinal); Assert.Contains("aria-keyshortcuts=\"F8\"", Markup, StringComparison.Ordinal); Assert.Contains("aria-keyshortcuts=\"Escape\"", Markup, StringComparison.Ordinal); Assert.Contains( ".connection-state.dry-run:not(.outcome-unknown)", Styles, StringComparison.Ordinal); Assert.Contains("accessibleStates.push(\"현재 PGM\")", App, StringComparison.Ordinal); Assert.Contains("accessibleStates.push(\"송출 후보\")", App, StringComparison.Ordinal); Assert.Contains("accessibleStates.push(\"삭제 선택됨\")", App, StringComparison.Ordinal); Assert.Matches( new Regex( @"id=""stock-search-button""[\s\S]*?]*viewBox=""0 0 24 24""", RegexOptions.CultureInvariant), Markup); Assert.Matches( new Regex( @"class=""playlist-heading-icon""[\s\S]*?]*viewBox=""0 0 24 24""", RegexOptions.CultureInvariant), Markup); } [Fact] public void NavigationChangesPresentationWithoutNativeOrStoredState() { Assert.Contains("entry.view.hidden = !isCurrent", Navigation, StringComparison.Ordinal); Assert.Contains( "entry.view.toggleAttribute(\"inert\", !isCurrent)", Navigation, StringComparison.Ordinal); Assert.Contains("settingsTab.addEventListener(\"click\"", Navigation, StringComparison.Ordinal); Assert.Contains("syncMarket(button.dataset.tabId)", Navigation, StringComparison.Ordinal); Assert.Contains("menuButton.click()", Navigation, StringComparison.Ordinal); Assert.Contains("pendingKeyboardMarketTabId", Navigation, StringComparison.Ordinal); Assert.Contains("pendingButton.focus()", Navigation, StringComparison.Ordinal); Assert.Contains("LegacyWorkspaceNavigation", Navigation, StringComparison.Ordinal); Assert.Contains("nav-collapsed", Navigation, StringComparison.Ordinal); Assert.DoesNotContain("postMessage", Navigation, StringComparison.Ordinal); Assert.DoesNotContain("localStorage", Navigation, StringComparison.Ordinal); Assert.DoesNotContain("sessionStorage", Navigation, StringComparison.Ordinal); } private static string FindRepositoryRoot() { var directory = new DirectoryInfo(AppContext.BaseDirectory); while (directory is not null) { if (File.Exists(Path.Combine(directory.FullName, "MBN_STOCK_WEBVIEW.sln"))) { return directory.FullName; } directory = directory.Parent; } throw new DirectoryNotFoundException("Repository root could not be located."); } }