namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests; public sealed class LegacyThemeWebParityTests { private static readonly string RepositoryRoot = FindRepositoryRoot(); private static readonly string WebRoot = Path.Combine( RepositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web"); private static readonly string App = File.ReadAllText(Path.Combine(WebRoot, "app.js")); private static readonly string Styles = File.ReadAllText(Path.Combine(WebRoot, "styles.css")); [Fact] public void Theme_workspace_matches_original_vertical_visible_order_and_action_order() { var renderTheme = Extract( App, " function renderTheme(state) {", " function renderExpert(state) {"); Assert.Contains("input.setAttribute(\"aria-label\", \"테마명\");", renderTheme, StringComparison.Ordinal); Assert.Contains("searchButton.textContent = \"테마검색\";", renderTheme, StringComparison.Ordinal); Assert.Contains("search.append(input, searchButton, addTheme, editTheme, deleteTheme);", renderTheme, StringComparison.Ordinal); Assert.Contains("resultsTitle.textContent = \"테마명\";", renderTheme, StringComparison.Ordinal); Assert.Contains("previewTitle.textContent = \"종목명\";", renderTheme, StringComparison.Ordinal); Assert.Contains("sortControls.className = \"theme-sort-controls\";", renderTheme, StringComparison.Ordinal); Assert.Contains("radio.type = \"radio\";", renderTheme, StringComparison.Ordinal); Assert.Contains( "workspace.append(search, resultsPanel, sortControls, actions, previewPanel);", renderTheme, StringComparison.Ordinal); Assert.DoesNotContain("theme-body", renderTheme, StringComparison.Ordinal); Assert.Contains( "grid-template-rows: auto minmax(72px, .8fr) auto auto minmax(72px, 1fr)", Styles, StringComparison.Ordinal); Assert.Contains("grid-template-columns: repeat(4, minmax(0, 1fr))", Styles, StringComparison.Ordinal); Assert.Contains( "[\"six-row-current\", 0],\n" + " [\"twelve-row-current\", 1],\n" + " [\"five-row-current\", 2],\n" + " [\"five-row-expected\", 3]", renderTheme, StringComparison.Ordinal); Assert.Contains("[\"six-row-current\", \"6종목 현재가\"]", renderTheme, StringComparison.Ordinal); Assert.Contains("[\"twelve-row-current\", \"12종목 현재가\"]", renderTheme, StringComparison.Ordinal); Assert.Contains("[\"five-row-current\", \"현재가\"]", renderTheme, StringComparison.Ordinal); Assert.Contains("[\"five-row-expected\", \"예상체결가\"]", renderTheme, StringComparison.Ordinal); } [Fact] public void Main_theme_workspace_does_not_expose_the_non_original_NXT_session_selector() { var renderTheme = Extract( App, " function renderTheme(state) {", " function renderExpert(state) {"); var clickDelegate = Extract( App, " categoryTree.addEventListener(\"click\", function (event) {", " categoryTree.addEventListener(\"change\", function (event) {"); Assert.DoesNotContain("theme-nxt-session", App, StringComparison.Ordinal); Assert.DoesNotContain("themeSearchNxtSession", App, StringComparison.Ordinal); Assert.DoesNotContain("nxtSession", clickDelegate, StringComparison.Ordinal); Assert.Contains("send(\"search-themes\", { query: input ? input.value : \"\" });", clickDelegate, StringComparison.Ordinal); } [Fact] public void Selection_receipt_updates_existing_theme_rows_without_replacing_click_target_or_focus() { var renderTheme = Extract( App, " function renderTheme(state) {", " function renderExpert(state) {"); var structureIdentity = Extract( renderTheme, " const structureKey = JSON.stringify({", " const existingWorkspace = categoryTree.querySelector(\".theme-workspace\");"); var selectionReceipt = Extract( renderTheme, " if (existingWorkspace && structureKey === themeRenderStructureKey) {", " const workspace = document.createElement(\"div\");"); var updater = Extract( App, " function updateThemeWorkspace(workspace, theme) {", " function renderTheme(state) {"); Assert.DoesNotContain("selectedResultId", structureIdentity, StringComparison.Ordinal); Assert.DoesNotContain("selectedTheme", structureIdentity, StringComparison.Ordinal); Assert.DoesNotContain("previewItems", structureIdentity, StringComparison.Ordinal); Assert.DoesNotContain("isAvailable", structureIdentity, StringComparison.Ordinal); Assert.Contains("updateThemeWorkspace(existingWorkspace, theme);", selectionReceipt, StringComparison.Ordinal); Assert.Contains("return;", selectionReceipt, StringComparison.Ordinal); Assert.DoesNotContain("replaceChildren", selectionReceipt, StringComparison.Ordinal); Assert.Contains("syncSearchDraft(input, theme.query)", updater, StringComparison.Ordinal); Assert.Contains("button.classList.toggle(\"selected\", selected);", updater, StringComparison.Ordinal); Assert.Contains("button.setAttribute(\"aria-selected\"", updater, StringComparison.Ordinal); Assert.DoesNotContain("results.replaceChildren", updater, StringComparison.Ordinal); Assert.DoesNotContain("categoryTree.replaceChildren", updater, StringComparison.Ordinal); } [Fact] public void NXT_expected_price_action_stays_clickable_so_native_can_show_the_legacy_warning() { var clickability = Extract( App, " function themeActionIsClickable(action) {", " function updateThemeWorkspace(workspace, theme) {"); var updater = Extract( App, " function updateThemeWorkspace(workspace, theme) {", " function renderTheme(state) {"); var clickDelegate = Extract( App, " categoryTree.addEventListener(\"click\", function (event) {", " categoryTree.addEventListener(\"change\", function (event) {"); Assert.Contains("action.actionId === \"five-row-expected\"", clickability, StringComparison.Ordinal); Assert.Contains("action.unavailableReason === \"expected-theme-is-krx-only\"", clickability, StringComparison.Ordinal); Assert.Contains("button.disabled = !themeActionIsClickable(action);", updater, StringComparison.Ordinal); Assert.Contains("? \"NXT는 데이터가 없습니다.\"", updater, StringComparison.Ordinal); Assert.Contains("send(\"activate-theme-action\"", clickDelegate, StringComparison.Ordinal); } private static string Extract(string source, string startToken, string endToken) { var start = source.IndexOf(startToken, StringComparison.Ordinal); var end = source.IndexOf(endToken, start + startToken.Length, StringComparison.Ordinal); Assert.True(start >= 0, $"Start token was not found: {startToken}"); Assert.True(end > start, $"End token was not found after start token: {endToken}"); 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 was not found."); } }