using System.Text.RegularExpressions; namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests; public sealed class LegacyOperatorSettingsWebContractTests { 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 SettingsHost = File.ReadAllText(Path.Combine( RepositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "MainWindow.Settings.cs")); [Fact] public void SettingsScreenKeepsThreeNativeFolderPickersWithConciseFeedback() { var expectedKinds = new[] { "design", "resource", "background" }; var actualKinds = Regex.Matches(Markup, "data-operator-folder=\"([^\"]+)\"") .Select(match => match.Groups[1].Value) .ToArray(); Assert.Equal(expectedKinds, actualKinds); foreach (var kind in expectedKinds) { Assert.Contains($"id=\"operator-{kind}-folder-name\"", Markup, StringComparison.Ordinal); Assert.Contains($"id=\"operator-{kind}-folder-status\"", Markup, StringComparison.Ordinal); Assert.Contains($"id=\"operator-{kind}-folder-select\"", Markup, StringComparison.Ordinal); Assert.Contains($"id=\"operator-{kind}-folder-reset\"", Markup, StringComparison.Ordinal); Assert.Matches( $"id=\"operator-{kind}-folder-source\"[^>]*hidden", Markup); } Assert.Matches( "id=\"operator-settings-message\"[^>]*role=\"status\"[^>]*aria-live=\"polite\"", Markup); Assert.Matches( "id=\"operator-settings-restart\"[^>]*role=\"status\"[^>]*aria-live=\"polite\"", Markup); Assert.Contains("

폴더

", Markup, StringComparison.Ordinal); Assert.Contains("

메뉴

", Markup, StringComparison.Ordinal); Assert.Contains("

상태

", Markup, StringComparison.Ordinal); Assert.Contains("재시작하면 적용됩니다.", Markup, StringComparison.Ordinal); Assert.Contains("id=\"operator-design-folder-name\">앱 기본값", Markup, StringComparison.Ordinal); Assert.Contains("id=\"operator-resource-folder-name\">앱 기본값", Markup, StringComparison.Ordinal); Assert.Contains("id=\"operator-background-folder-name\">자동", Markup, StringComparison.Ordinal); Assert.Contains("id=\"operator-navigation-expanded\"", Markup, StringComparison.Ordinal); Assert.Contains("메뉴 펼치기", Markup, StringComparison.Ordinal); Assert.Contains("settings-status-badge.valid", Styles, StringComparison.Ordinal); Assert.Contains("settings-status-badge.invalid", Styles, StringComparison.Ordinal); Assert.DoesNotContain("settings-intro", Markup, StringComparison.Ordinal); Assert.DoesNotContain("이 PC에서 사용할 디자인·설정·배경 폴더", Markup, StringComparison.Ordinal); Assert.DoesNotContain("경로를 직접 입력하지 않고", Markup, StringComparison.Ordinal); Assert.DoesNotContain("송출 장면(.t2s)과 장면별 디자인 자산", Markup, StringComparison.Ordinal); Assert.DoesNotContain("핵심 자산 확인됨", Markup, StringComparison.Ordinal); } [Fact] public void SettingsDispatchesOnlyClosedFolderKindsAndNeverWebSuppliedPaths() { var dispatch = Slice( App, "Object.keys(operatorFolderControls).forEach(function (kind)", "searchButton.addEventListener"); Assert.Contains("send(\"choose-operator-folder\", { kind: kind })", dispatch, StringComparison.Ordinal); Assert.Contains("send(\"reset-operator-folder\", { kind: kind })", dispatch, StringComparison.Ordinal); Assert.Contains( "send(\"set-operator-navigation-expanded\", { expanded: expanded })", dispatch, StringComparison.Ordinal); Assert.DoesNotContain("path", dispatch, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("displayName", dispatch, StringComparison.Ordinal); } [Fact] public void AppearanceSettingsExposeThreeConciseClosedChoices() { var appearance = Slice( Markup, "
", "
"); Assert.Contains("

화면

", appearance, StringComparison.Ordinal); Assert.Contains("id=\"operator-color-theme\"", appearance, StringComparison.Ordinal); Assert.Contains("value=\"system\">자동", appearance, StringComparison.Ordinal); Assert.Contains("value=\"light\">라이트", appearance, StringComparison.Ordinal); Assert.Contains("value=\"dark\">다크", appearance, StringComparison.Ordinal); Assert.Contains("id=\"operator-view-mode\"", appearance, StringComparison.Ordinal); Assert.Contains("value=\"automatic\">자동", appearance, StringComparison.Ordinal); Assert.Contains("value=\"compact\">컴팩트", appearance, StringComparison.Ordinal); Assert.Contains("value=\"cards\">카드", appearance, StringComparison.Ordinal); Assert.Contains("id=\"operator-start-workspace\"", appearance, StringComparison.Ordinal); Assert.Contains("value=\"stockCut\">종목·컷", appearance, StringComparison.Ordinal); Assert.Contains("value=\"lastWorkspace\">마지막 화면", appearance, StringComparison.Ordinal); Assert.DoesNotContain("()); Assert.Contains("pendingOperatorSettingsFocusId", settingsRender, StringComparison.Ordinal); Assert.Contains("target.focus({ preventScroll: true });", settingsRender, StringComparison.Ordinal); } [Fact] public void SettingsKeepsSafeDiagnosticsWithoutProtectedValueExplanations() { foreach (var id in new[] { "operator-settings-app-version", "operator-settings-playout-mode", "operator-settings-playout-connection", "operator-settings-oracle-state", "operator-settings-maria-state", "operator-settings-db-monitor" }) { Assert.Contains($"id=\"{id}\"", Markup, StringComparison.Ordinal); } Assert.Contains("id=\"operator-navigation-expanded\"", Markup, StringComparison.Ordinal); Assert.Contains("databasePollingSeconds", App, StringComparison.Ordinal); Assert.Contains("LegacyOperatorFolderValidator.TryValidate", SettingsHost, StringComparison.Ordinal); Assert.Contains("CanReadSelectedDatabaseProfile", SettingsHost, StringComparison.Ordinal); Assert.Contains("ResolveDatabaseHealthLabel", SettingsHost, StringComparison.Ordinal); Assert.Contains("\"저장됨\"", SettingsHost, StringComparison.Ordinal); Assert.Contains("\"저장됨 · 재시작 필요\"", SettingsHost, StringComparison.Ordinal); Assert.Contains("\"초기화됨 · 재시작 필요\"", SettingsHost, StringComparison.Ordinal); Assert.Contains("\"사용 가능\"", SettingsHost, StringComparison.Ordinal); Assert.DoesNotContain("외부 영상", SettingsHost, StringComparison.Ordinal); Assert.DoesNotContain("보호되는 운영 설정", Markup, StringComparison.Ordinal); Assert.DoesNotContain("DB 자격 증명", Markup, StringComparison.Ordinal); Assert.DoesNotContain("Tornado2 송출 대상", Markup, StringComparison.Ordinal); Assert.DoesNotContain("Live/DryRun 권한", Markup, StringComparison.Ordinal); Assert.DoesNotContain("장면 허용 목록", Markup, StringComparison.Ordinal); } private static string Slice(string source, string startMarker, string endMarker) { var start = source.IndexOf(startMarker, StringComparison.Ordinal); Assert.True(start >= 0, $"Missing start marker: {startMarker}"); var end = source.IndexOf(endMarker, start, StringComparison.Ordinal); Assert.True(end > start, $"Missing end marker: {endMarker}"); return source[start..end]; } 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."); } }