namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests; public sealed class LegacyPlayoutWebContractTests { 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 Script = File.ReadAllText( Path.Combine(WebRoot, "playout-ui.js")); [Fact] public void ShellExposesBackgroundControlsButKeepsOriginalHiddenFadeStateInert() { foreach (var id in new[] { "playout-connection-state", "playout-background-none", "playout-background-file", "playout-background-name", "playout-fade-duration", "playout-prepare", "playout-take-in", "playout-next", "playout-take-out", "playout-status" }) { Assert.Contains($"id=\"{id}\"", Markup, StringComparison.Ordinal); } Assert.Contains("PREPARE", Markup, StringComparison.Ordinal); Assert.Contains("TAKE IN", Markup, StringComparison.Ordinal); Assert.Contains("NEXT", Markup, StringComparison.Ordinal); Assert.Contains("TAKE OUT", Markup, StringComparison.Ordinal); var fadeMarkup = ExtractElement(Markup, "playout-fade-duration", ""); Assert.Equal(20, CountOccurrences(fadeMarkup, "", fadeMarkup, StringComparison.Ordinal); } Assert.Contains("", Markup, StringComparison.Ordinal); } [Fact] public void PlayoutWebLayerSendsCommandIntentOnly() { foreach (var intent in new[] { "prepare-playout", "take-in", "next-playout", "take-out", "choose-background", "toggle-background" }) { Assert.Contains($"\"{intent}\"", Script, StringComparison.Ordinal); } Assert.DoesNotContain("fetch(", Script, StringComparison.Ordinal); Assert.DoesNotContain("localStorage", Script, StringComparison.Ordinal); Assert.DoesNotContain("sessionStorage", Script, StringComparison.Ordinal); Assert.DoesNotContain("filePath", Script, StringComparison.Ordinal); Assert.DoesNotContain("sceneCode", Script, StringComparison.Ordinal); Assert.Contains("post(\"prepare-playout\", {})", Script, StringComparison.Ordinal); Assert.Contains("post(\"take-in\", {})", Script, StringComparison.Ordinal); Assert.Contains("post(\"next-playout\", {})", Script, StringComparison.Ordinal); Assert.Contains("post(\"take-out\", {})", Script, StringComparison.Ordinal); Assert.DoesNotContain("post(\"set-fade-duration\"", Script, StringComparison.Ordinal); Assert.DoesNotContain("fadeDuration.addEventListener", Script, StringComparison.Ordinal); Assert.Contains("fadeDuration.disabled = true;", Script, StringComparison.Ordinal); Assert.Contains("fadeDuration.value = String(playout.fadeDuration);", Script, StringComparison.Ordinal); } [Fact] public void OriginalGlobalShortcutsAreIsolatedFromOpenDialogs() { Assert.Contains("hasOpenDialog()", Script, StringComparison.Ordinal); Assert.Contains("event.key === \"F2\"", Script, StringComparison.Ordinal); Assert.Contains("event.key === \"F3\"", Script, StringComparison.Ordinal); Assert.Contains("event.key === \"F8\"", Script, StringComparison.Ordinal); Assert.Contains("event.key === \"Escape\"", Script, StringComparison.Ordinal); Assert.Contains("if (event.defaultPrevented) return;", Script, StringComparison.Ordinal); Assert.Contains("if (hasOpenDialog())", Script, StringComparison.Ordinal); Assert.Contains("if (legacyShortcut) event.preventDefault();", Script, StringComparison.Ordinal); } [Fact] public void ButtonsFollowNativePhaseCallbackAndOutcomeState() { Assert.Contains("playout.outcomeUnknown !== true", Script, StringComparison.Ordinal); Assert.Contains("const canPrepareToggle = playout.phase === \"idle\"", Script, StringComparison.Ordinal); Assert.Contains("canTakeInFromState(playout, hasPlaylist)", Script, StringComparison.Ordinal); Assert.Contains("playout.phase === \"idle\" || playout.phase === \"program\"", Script, StringComparison.Ordinal); Assert.Contains("canAdvanceFromState(playout)", Script, StringComparison.Ordinal); Assert.Contains("playout.isPlayCompletionPending === true", Script, StringComparison.Ordinal); Assert.Contains("playout.isTakeOutCompletionPending !== true", Script, StringComparison.Ordinal); Assert.Contains("playout.nextKind === \"none\"", Script, StringComparison.Ordinal); Assert.Contains("playout.nextKind === \"endOfPlaylist\"", Script, StringComparison.Ordinal); Assert.Contains("takeOut.disabled = busy || !commandAvailable;", Script, StringComparison.Ordinal); Assert.Contains("마지막 항목", Script, StringComparison.Ordinal); } private static int CountOccurrences(string source, string value) { var count = 0; var offset = 0; while ((offset = source.IndexOf(value, offset, StringComparison.Ordinal)) >= 0) { count++; offset += value.Length; } return count; } private static string ExtractElement(string source, string id, string closingTag) { var start = source.IndexOf($"id=\"{id}\"", StringComparison.Ordinal); Assert.True(start >= 0, $"Element '{id}' was not found."); var end = source.IndexOf(closingTag, start, StringComparison.Ordinal); Assert.True(end >= 0, $"Closing tag for '{id}' was not found."); return source[start..(end + closingTag.Length)]; } private static string FindRepositoryRoot() { var current = new DirectoryInfo(AppContext.BaseDirectory); while (current is not null) { if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln"))) { return current.FullName; } current = current.Parent; } throw new DirectoryNotFoundException("Repository root was not found."); } }