feat: advance legacy UI behavior parity

This commit is contained in:
2026-07-18 10:18:29 +09:00
parent d7ec571343
commit 1302b1b92f
71 changed files with 11244 additions and 1153 deletions

View File

@@ -0,0 +1,114 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyComparisonConfirmationNativeContractTests
{
private static readonly string RepositoryRoot = FindRepositoryRoot();
private static readonly string Controller = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyApplication",
"LegacyOperatorController.cs"));
private static readonly string MainWindow = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.xaml.cs"));
private static readonly string App = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"Web",
"app.js"));
private static readonly string Markup = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"Web",
"index.html"));
[Fact]
public void Uc3DeleteAllUsesTheExactNativeMessageBoxContract()
{
var request = Slice(
Controller,
"public async Task<LegacyOperatorSnapshot> DeleteComparisonPairAsync(",
"public LegacyOperatorSnapshot ActivateComparisonAction(");
Assert.Contains("if (_dialog is not null)", request, StringComparison.Ordinal);
Assert.Contains("PendingNativeConfirmation.ForComparisonDeleteAll(", request,
StringComparison.Ordinal);
Assert.Contains("\"모두 삭제하겠습니까?\"", request, StringComparison.Ordinal);
Assert.Contains("\"MmoneyCoder\"", request, StringComparison.Ordinal);
Assert.Contains("IsConfirmation: true", request, StringComparison.Ordinal);
Assert.DoesNotContain("DeleteAllPairsAsync", request, StringComparison.Ordinal);
}
[Fact]
public void NativeModalAllowsOnlyReadyAndItsEmptyYesOrNoDecision()
{
Assert.Contains(
"_controller.Current.Dialog?.IsConfirmation == true",
MainWindow,
StringComparison.Ordinal);
Assert.Contains(
"intent is not LegacyReadyIntent and",
MainWindow,
StringComparison.Ordinal);
Assert.Contains(
"not LegacyConfirmNativeDialogIntent and",
MainWindow,
StringComparison.Ordinal);
Assert.Contains(
"not LegacyCancelNativeDialogIntent",
MainWindow,
StringComparison.Ordinal);
Assert.Contains("? \"confirm-native-dialog\"", App, StringComparison.Ordinal);
Assert.Contains("send(\"cancel-native-dialog\", {});", App,
StringComparison.Ordinal);
Assert.DoesNotContain(
"window.confirm(\"저장된 종목 비교쌍을 모두 삭제하겠습니까?\")",
App,
StringComparison.Ordinal);
Assert.Contains(
"if (modal === legacyAlertDialog) return dialogOk;",
App,
StringComparison.Ordinal);
Assert.Contains(
"modalFocusManager.getActiveModal() === legacyAlertDialog &&\n !legacyDialogIsConfirmation",
App.Replace("\r\n", "\n", StringComparison.Ordinal),
StringComparison.Ordinal);
Assert.True(
Markup.IndexOf("id=\"legacy-dialog-ok\"", StringComparison.Ordinal) <
Markup.IndexOf("id=\"legacy-dialog-no\"", StringComparison.Ordinal));
Assert.Contains(
"dialogOk.textContent = legacyDialogIsConfirmation ? \"예\" : \"확인\"",
App,
StringComparison.Ordinal);
Assert.Contains(">아니요</button>", Markup, StringComparison.Ordinal);
}
private static string Slice(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 && end > start);
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("Could not locate the repository root.");
}
}

View File

@@ -0,0 +1,192 @@
using System.Text.RegularExpressions;
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyIndustryListWebContractTests
{
private static readonly string RepositoryRoot = FindRepositoryRoot();
private static readonly string App = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"Web",
"app.js"));
[Fact]
public void Industry_list_is_one_tab_stop_and_rows_are_programmatic_descendants()
{
var renderIndustry = Extract(
App,
" function renderIndustry(state) {",
" function renderOverseas(state) {");
var listFactory = Extract(
renderIndustry,
" const list = document.createElement(\"div\");",
" const comparisonTitle = document.createElement(\"strong\");");
Assert.Contains("list.setAttribute(\"role\", \"listbox\");", listFactory,
StringComparison.Ordinal);
Assert.Contains("list.tabIndex = 0;", listFactory, StringComparison.Ordinal);
Assert.Contains("industry.industries.forEach(function (row) {", listFactory,
StringComparison.Ordinal);
Assert.Contains("button.dataset.industryIndex = String(row.index);", listFactory,
StringComparison.Ordinal);
Assert.Contains("button.tabIndex = -1;", listFactory, StringComparison.Ordinal);
Assert.Contains("button.setAttribute(\"role\", \"option\");", listFactory,
StringComparison.Ordinal);
Assert.Single(Regex.Matches(listFactory, @"\.tabIndex\s*=\s*0;").Cast<Match>());
Assert.DoesNotContain("button.tabIndex = 0;", listFactory, StringComparison.Ordinal);
}
[Fact]
public void Industry_list_handles_arrow_home_and_end_navigation()
{
var keyHandler = Extract(
App,
" categoryTree.addEventListener(\"keydown\", function (event) {",
" window.addEventListener(\"pointerup\", function () {");
Assert.Contains("const industryList = event.target.closest(\".industry-list\");",
keyHandler, StringComparison.Ordinal);
Assert.Contains(
"[\"ArrowUp\", \"ArrowDown\", \"Home\", \"End\"].includes(event.key)",
keyHandler,
StringComparison.Ordinal);
Assert.Contains("event.preventDefault();", keyHandler, StringComparison.Ordinal);
Assert.Contains("if (event.key === \"Home\") nextIndex = 0;", keyHandler,
StringComparison.Ordinal);
Assert.Contains("else if (event.key === \"End\") nextIndex = rows.length - 1;",
keyHandler, StringComparison.Ordinal);
Assert.Contains(
"else if (event.key === \"ArrowUp\") nextIndex = Math.max(0, selectedIndex - 1);",
keyHandler,
StringComparison.Ordinal);
Assert.Contains(
"else nextIndex = Math.min(rows.length - 1, selectedIndex + 1);",
keyHandler,
StringComparison.Ordinal);
Assert.Contains("selectIndustryRow(nextRow);", keyHandler, StringComparison.Ordinal);
Assert.Contains("nextRow.scrollIntoView({ block: \"nearest\", inline: \"nearest\" });",
keyHandler, StringComparison.Ordinal);
}
[Fact]
public void Selection_only_state_receipt_updates_rows_without_replacing_the_workspace()
{
var renderIndustry = Extract(
App,
" function renderIndustry(state) {",
" function renderOverseas(state) {");
var structureIdentity = Extract(
renderIndustry,
" const structureKey = JSON.stringify({",
" const existingWorkspace = categoryTree.querySelector(\".industry-workspace\");");
var selectionOnlyReceipt = Extract(
renderIndustry,
" if (existingWorkspace && structureKey === industryRenderStructureKey) {",
" const previousList = categoryTree.querySelector(\".industry-list\");");
Assert.Contains("market: industry.market", structureIdentity, StringComparison.Ordinal);
Assert.Contains("rows: industry.industries.map", structureIdentity,
StringComparison.Ordinal);
Assert.Contains("actions: industry.actions.map", structureIdentity,
StringComparison.Ordinal);
Assert.DoesNotContain("selectedIndex", structureIdentity, StringComparison.Ordinal);
Assert.Contains("existingWorkspace.querySelectorAll(\"button[data-industry-index]\")",
selectionOnlyReceipt, StringComparison.Ordinal);
Assert.Contains(
"const selected = Number(row.dataset.industryIndex) === industry.selectedIndex;",
selectionOnlyReceipt,
StringComparison.Ordinal);
Assert.Contains("row.classList.toggle(\"selected\", selected);", selectionOnlyReceipt,
StringComparison.Ordinal);
Assert.Contains(
"row.setAttribute(\"aria-selected\", selected ? \"true\" : \"false\");",
selectionOnlyReceipt,
StringComparison.Ordinal);
Assert.Contains("return;", selectionOnlyReceipt, StringComparison.Ordinal);
Assert.DoesNotContain("replaceChildren", selectionOnlyReceipt, StringComparison.Ordinal);
Assert.DoesNotContain("document.createElement", selectionOnlyReceipt,
StringComparison.Ordinal);
}
[Fact]
public void Industry_list_is_appended_before_comparison_label_and_slots()
{
var renderIndustry = Extract(
App,
" function renderIndustry(state) {",
" function renderOverseas(state) {");
Assert.Contains("comparisonTitle.textContent = \"업종비교\";", renderIndustry,
StringComparison.Ordinal);
Assert.Contains("selector.append(list, comparisonTitle, slots);", renderIndustry,
StringComparison.Ordinal);
Assert.DoesNotContain("selector.append(comparisonTitle, slots, list);", renderIndustry,
StringComparison.Ordinal);
}
[Fact]
public void Comparison_text_boxes_commit_closed_text_intents_without_replacing_focused_dom()
{
var renderIndustry = Extract(
App,
" function renderIndustry(state) {",
" function renderOverseas(state) {");
var changeHandler = Extract(
App,
" categoryTree.addEventListener(\"change\", function (event) {",
" categoryTree.addEventListener(\"keydown\", function (event) {");
Assert.Contains("first.dataset.industrySlot = \"first\";", renderIndustry,
StringComparison.Ordinal);
Assert.Contains("second.dataset.industrySlot = \"second\";", renderIndustry,
StringComparison.Ordinal);
Assert.Contains("first.maxLength = 128;", renderIndustry, StringComparison.Ordinal);
Assert.Contains("second.maxLength = 128;", renderIndustry, StringComparison.Ordinal);
Assert.DoesNotContain("first.readOnly = true;", renderIndustry,
StringComparison.Ordinal);
Assert.DoesNotContain("second.readOnly = true;", renderIndustry,
StringComparison.Ordinal);
Assert.DoesNotContain("firstName: industry.firstName", renderIndustry,
StringComparison.Ordinal);
Assert.DoesNotContain("secondName: industry.secondName", renderIndustry,
StringComparison.Ordinal);
Assert.Contains("document.activeElement !== firstInput", renderIndustry,
StringComparison.Ordinal);
Assert.Contains("document.activeElement !== secondInput", renderIndustry,
StringComparison.Ordinal);
Assert.Contains("send(\"set-industry-comparison-text\"", changeHandler,
StringComparison.Ordinal);
Assert.Contains("slot: event.target.dataset.industrySlot", changeHandler,
StringComparison.Ordinal);
Assert.Contains("value: event.target.value", changeHandler,
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.");
}
}

View File

@@ -51,10 +51,18 @@ public sealed class LegacyOperatorCatalogParityWebTests
Assert.DoesNotContain("ThemeA 추가·편집·삭제", App, StringComparison.Ordinal);
Assert.DoesNotContain("EList 추가·편집·삭제", App, StringComparison.Ordinal);
foreach (var id in new[]
{
"uc4-theme-add", "uc4-theme-edit", "uc4-theme-delete",
"uc6-expert-add", "uc6-expert-edit", "uc6-expert-delete"
})
{
Assert.Contains($".id = \"{id}\"", App, StringComparison.Ordinal);
}
}
[Fact]
public void Modal_contains_search_create_edit_delete_stock_and_reorder_controls()
public void Modal_contains_search_create_edit_stock_and_reorder_controls_without_child_delete()
{
foreach (var id in new[]
{
@@ -70,7 +78,7 @@ public sealed class LegacyOperatorCatalogParityWebTests
"operator-catalog-add-stock",
"operator-catalog-draft-rows",
"operator-catalog-save",
"operator-catalog-delete"
"operator-catalog-delete-all"
})
{
Assert.Contains($"id=\"{id}\"", Markup, StringComparison.Ordinal);
@@ -91,7 +99,21 @@ public sealed class LegacyOperatorCatalogParityWebTests
StringComparison.Ordinal);
Assert.Contains(".operator-catalog-draft-row.drag-before", Styles,
StringComparison.Ordinal);
Assert.Contains("remove.textContent = isTheme ? \"삭제\" : \"종목삭제\"", App,
Assert.Contains("remove.textContent = \"Delete\"", App, StringComparison.Ordinal);
Assert.Contains("up.textContent = \"Up\"", App, StringComparison.Ordinal);
Assert.Contains("down.textContent = \"Down\"", App, StringComparison.Ordinal);
Assert.Contains("operatorCatalogStockSearchLine.hidden = !showCatalogRows", App,
StringComparison.Ordinal);
Assert.Contains("catalog.mode === \"edit\"", App, StringComparison.Ordinal);
Assert.DoesNotContain("id=\"operator-catalog-delete\"", Markup,
StringComparison.Ordinal);
Assert.Contains("send(\"clear-operator-catalog-draft-rows\", {})", App,
StringComparison.Ordinal);
Assert.Contains("const isActive = row.rowId === catalog.activeDraftRowId", App,
StringComparison.Ordinal);
Assert.Contains("send(\"select-operator-catalog-draft-row\", { rowId: row.rowId })", App,
StringComparison.Ordinal);
Assert.Contains("send(\"remove-active-operator-catalog-draft-row\", {})", App,
StringComparison.Ordinal);
}
@@ -112,10 +134,14 @@ public sealed class LegacyOperatorCatalogParityWebTests
"reorder-operator-catalog-draft-row",
"move-operator-catalog-draft-row",
"remove-operator-catalog-draft-row",
"select-operator-catalog-draft-row",
"remove-active-operator-catalog-draft-row",
"clear-operator-catalog-draft-rows",
"set-operator-catalog-draft-buy-amount",
"check-operator-catalog-theme-name",
"save-operator-catalog",
"delete-operator-catalog"
"confirm-native-dialog",
"cancel-native-dialog"
})
{
Assert.Contains($"\"{intent}\"", App, StringComparison.Ordinal);
@@ -133,10 +159,68 @@ public sealed class LegacyOperatorCatalogParityWebTests
"send(\"check-operator-catalog-theme-name\", { name: name });",
App,
StringComparison.Ordinal);
Assert.Contains("send(\"close-operator-catalog\", {})", App,
StringComparison.Ordinal);
Assert.Contains("rowId: remove.dataset.operatorCatalogRemoveRowId", App,
StringComparison.Ordinal);
Assert.DoesNotContain(
"operatorCatalogCancel.addEventListener(\"click\", function () {\n" +
" operatorCatalogDraftKey = \"\";\n" +
" searchOperatorCatalog();",
App,
StringComparison.Ordinal);
Assert.DoesNotContain(
"send(\"check-operator-catalog-theme-name\", { name: name,",
App,
StringComparison.Ordinal);
Assert.DoesNotContain("delete-operator-catalog", App, StringComparison.Ordinal);
Assert.DoesNotContain("window.confirm(\"선택한 테마를 삭제하겠습니까?\")", App,
StringComparison.Ordinal);
Assert.DoesNotContain("window.confirm(\"선택한 전문가를 삭제하겠습니까?\")", App,
StringComparison.Ordinal);
Assert.DoesNotContain("window.confirm(\"모두 삭제하겠습니까?\")", App,
StringComparison.Ordinal);
Assert.DoesNotContain(
"window.confirm(\"선택한 ThemeA/EList 항목을 DB에서 삭제하시겠습니까?\")",
App,
StringComparison.Ordinal);
}
[Fact]
public void Native_dialog_renders_caption_and_confirmation_and_sends_only_empty_decisions()
{
foreach (var id in new[]
{
"legacy-dialog-caption", "legacy-dialog-message",
"legacy-dialog-ok", "legacy-dialog-no"
})
{
Assert.Contains($"id=\"{id}\"", Markup, StringComparison.Ordinal);
}
Assert.Contains("state.dialog.isConfirmation === true", App, StringComparison.Ordinal);
Assert.Contains("dialogCaption.textContent = state.dialog.caption || \"MmoneyCoder\"", App,
StringComparison.Ordinal);
Assert.Contains("dialogOk.textContent = legacyDialogIsConfirmation ? \"예\" : \"확인\"", App,
StringComparison.Ordinal);
Assert.Contains("dialogNo.hidden = !legacyDialogIsConfirmation", App,
StringComparison.Ordinal);
Assert.Contains("? \"confirm-native-dialog\"", App,
StringComparison.Ordinal);
Assert.Contains("send(\"cancel-native-dialog\", {});", App,
StringComparison.Ordinal);
var confirmStart = App.IndexOf(
"dialogOk.addEventListener(\"click\"",
StringComparison.Ordinal);
var confirmEnd = App.IndexOf("if (webview)", confirmStart, StringComparison.Ordinal);
Assert.True(confirmStart >= 0 && confirmEnd > confirmStart);
var decisionHandlers = App[confirmStart..confirmEnd];
Assert.DoesNotMatch(
new Regex("(?:themeCode|expertCode|stockCode|itemCode|rowId|expectedIdentity)"),
decisionHandlers);
Assert.Contains(": \"dismiss-dialog\", {});", decisionHandlers,
StringComparison.Ordinal);
}
[Fact]
@@ -163,7 +247,16 @@ public sealed class LegacyOperatorCatalogParityWebTests
Assert.Contains("LegacyCheckOperatorCatalogThemeNameIntent", MainWindow,
StringComparison.Ordinal);
Assert.Contains("LegacySaveOperatorCatalogIntent", MainWindow, StringComparison.Ordinal);
Assert.Contains("LegacyDeleteOperatorCatalogIntent", MainWindow, StringComparison.Ordinal);
Assert.Contains("LegacySelectOperatorCatalogDraftRowIntent", MainWindow,
StringComparison.Ordinal);
Assert.Contains("LegacyRemoveActiveOperatorCatalogDraftRowIntent", MainWindow,
StringComparison.Ordinal);
Assert.Contains("LegacyConfirmNativeDialogIntent", MainWindow,
StringComparison.Ordinal);
Assert.Contains("LegacyCancelNativeDialogIntent", MainWindow,
StringComparison.Ordinal);
Assert.DoesNotContain("LegacyDeleteOperatorCatalogIntent", MainWindow,
StringComparison.Ordinal);
Assert.Contains("_intentGate.WaitAsync(", MainWindow, StringComparison.Ordinal);
}
@@ -185,9 +278,11 @@ public sealed class LegacyOperatorCatalogParityWebTests
"LegacyReorderOperatorCatalogDraftRowIntent",
"LegacyMoveOperatorCatalogDraftRowIntent",
"LegacyRemoveOperatorCatalogDraftRowIntent",
"LegacyRemoveActiveOperatorCatalogDraftRowIntent",
"LegacyClearOperatorCatalogDraftRowsIntent",
"LegacySetOperatorCatalogDraftBuyAmountIntent",
"LegacySaveOperatorCatalogIntent",
"LegacyDeleteOperatorCatalogIntent",
"LegacyConfirmNativeDialogIntent",
"LegacyActivateThemeActionIntent",
"LegacyActivateExpertActionIntent"
})

View File

@@ -0,0 +1,48 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyOverseasAllNationPlayoutContractTests
{
[Fact]
public void NativeRuntimeKeepsUc5ExactIdentityForOneColumnAndCandleRoutes()
{
var root = FindRepositoryRoot();
var runtime = File.ReadAllText(Path.Combine(root, "LegacySceneRuntimeFactory.cs"));
var resolver = File.ReadAllText(Path.Combine(
root,
"src",
"MBN_STOCK_WEBVIEW.Core",
"Playout",
"Scenes",
"LegacyParameterizedSceneRequestResolver.cs"));
Assert.Contains("parameterizedRequestResolver.CreateS5001Request(entry)", runtime,
StringComparison.Ordinal);
Assert.Contains("[\"5001\", \"N5001\"]", runtime, StringComparison.Ordinal);
Assert.Contains("[\"8035\", \"8061\", \"8040\", \"8046\", \"8051\", \"8056\"]", runtime,
StringComparison.Ordinal);
Assert.Contains("ParseForeignCandleIdentity(selection.DataCode, target)", runtime,
StringComparison.Ordinal);
Assert.Contains("LegacyNamedForeignStockRestoreService.IsCanonicalBoundSymbol(parts[0])", runtime,
StringComparison.Ordinal);
Assert.DoesNotContain("parts[1] is not (\"US\" or \"TW\")", runtime,
StringComparison.Ordinal);
Assert.Contains("new S5001ExactForeignStockLoadRequest(", resolver,
StringComparison.Ordinal);
}
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.");
}
}

View File

@@ -9,27 +9,38 @@ public sealed class LegacyOverseasParityWebTests
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"Web");
private static readonly string App = File.ReadAllText(Path.Combine(WebRoot, "app.js"));
private static readonly string Markup = File.ReadAllText(Path.Combine(WebRoot, "index.html"));
private static readonly string Styles = File.ReadAllText(Path.Combine(WebRoot, "styles.css"));
[Fact]
public void OverseasTabRendersFixedIndustryStockAndActionWorkspaces()
public void OverseasTabUsesTheOriginalThreeVerticalGroups()
{
Assert.Contains("data-tab-id=\"overseasStocks\">해외종목</button>", Markup);
Assert.Contains("function renderOverseas(state)", App, StringComparison.Ordinal);
Assert.Contains("overseas-fixed-list", App, StringComparison.Ordinal);
Assert.Contains("미국 해외업종", App, StringComparison.Ordinal);
Assert.Contains("미국·대만 해외종목", App, StringComparison.Ordinal);
Assert.Contains("overseas-action-panel", App, StringComparison.Ordinal);
Assert.Contains("renderOverseas(state);", App, StringComparison.Ordinal);
var render = Section("function renderOverseas(state)", "function renderComparison(state)");
Assert.Contains("overseas-fixed-group", render, StringComparison.Ordinal);
Assert.Contains("overseas-search-group", render, StringComparison.Ordinal);
Assert.Contains("\uD574\uC678\uC9C0\uC218", render, StringComparison.Ordinal);
Assert.Contains("\uD574\uC678 \uC5C5\uC885\uC9C0\uC218", render, StringComparison.Ordinal);
Assert.Contains("\uD574\uC678 \uC885\uBAA9", render, StringComparison.Ordinal);
Assert.Contains("\uC5C5\uC885 \uAC80\uC0C9", render, StringComparison.Ordinal);
Assert.Contains("\uC885\uBAA9 \uAC80\uC0C9", render, StringComparison.Ordinal);
Assert.Contains("\u0031\uC5F4\uD310", render, StringComparison.Ordinal);
Assert.Contains("periodButtons(\"fixedIndex\")", render, StringComparison.Ordinal);
Assert.Contains("periodButtons(kind)", render, StringComparison.Ordinal);
Assert.Contains("model.isTruncated === true", render, StringComparison.Ordinal);
Assert.Contains("검색 결과가 최대 500건으로 제한되었습니다.", render, StringComparison.Ordinal);
Assert.Contains("overseas-truncation", Styles, StringComparison.Ordinal);
Assert.DoesNotContain("overseas-action-panel", render, StringComparison.Ordinal);
Assert.DoesNotContain("overseas-selected", render, StringComparison.Ordinal);
Assert.DoesNotContain("overseas-note", render, StringComparison.Ordinal);
Assert.DoesNotContain("document.createElement(\"details\")", render, StringComparison.Ordinal);
foreach (var selector in new[]
{
".overseas-workspace",
".overseas-fixed-list",
".overseas-searches",
".overseas-results",
".overseas-action-panel"
".overseas-fixed-body",
".overseas-period-actions",
".overseas-result-area",
".overseas-results"
})
{
Assert.Contains(selector, Styles, StringComparison.Ordinal);
@@ -37,8 +48,28 @@ public sealed class LegacyOverseasParityWebTests
}
[Fact]
public void OverseasEventsSendOnlyQueriesAndOpaqueIds()
public void OverseasRowsSelectImmediatelyAndOnlyDispatchOpaqueNativeIdentifiers()
{
var render = Section("function renderOverseas(state)", "function renderComparison(state)");
var click = Section(
"categoryTree.addEventListener(\"click\"",
"categoryTree.addEventListener(\"change\"");
var overseasClick = click[click.IndexOf(
"const overseasTarget = event.target.closest(\"button[data-overseas-target-id]\");",
StringComparison.Ordinal)..click.IndexOf("const comparisonChoice", StringComparison.Ordinal)];
var updater = Section("function updateOverseasWorkspace", "function markOverseasRowSelected");
Assert.Contains("function selectOverseasRow(row)", App, StringComparison.Ordinal);
Assert.Contains("markOverseasRowSelected(row);", App, StringComparison.Ordinal);
Assert.Contains("selectOverseasRow(overseasTarget);", overseasClick, StringComparison.Ordinal);
Assert.Contains("selectOverseasRow(overseasResult);", overseasClick, StringComparison.Ordinal);
Assert.DoesNotContain("setTimeout", overseasClick, StringComparison.Ordinal);
Assert.Contains("existingWorkspace && structureKey === overseasRenderStructureKey", render,
StringComparison.Ordinal);
Assert.Contains("updateOverseasWorkspace(existingWorkspace, overseas", render,
StringComparison.Ordinal);
Assert.DoesNotContain("categoryTree.replaceChildren", updater, StringComparison.Ordinal);
foreach (var intent in new[]
{
"select-overseas-fixed-index",
@@ -52,23 +83,44 @@ public sealed class LegacyOverseasParityWebTests
Assert.Contains($"\"{intent}\"", App, StringComparison.Ordinal);
}
Assert.Contains("data-overseas-action-id", App, StringComparison.Ordinal);
Assert.DoesNotContain("nationCode", render, StringComparison.Ordinal);
Assert.DoesNotContain("FOREIGN_", render, StringComparison.Ordinal);
Assert.DoesNotContain("symbol", render, StringComparison.OrdinalIgnoreCase);
Assert.Contains("event.key === \"Enter\"", App, StringComparison.Ordinal);
var clickStart = App.IndexOf(
"categoryTree.addEventListener(\"click\"",
}
[Fact]
public void OverseasListsUseOneTabStopAndNativeSingleSelectionKeyboardSemantics()
{
var helper = Section("function moveOverseasListSelection", "function renderOverseas(state)");
var keyDown = Section(
"categoryTree.addEventListener(\"keydown\"",
"window.addEventListener(\"pointerup\"");
Assert.Contains("list.getAttribute(\"aria-disabled\") === \"true\"", helper,
StringComparison.Ordinal);
var changeStart = App.IndexOf(
"categoryTree.addEventListener(\"change\"",
clickStart,
Assert.Contains("list.querySelectorAll(\"button\")", helper, StringComparison.Ordinal);
Assert.Contains("selectOverseasRow(nextRow);", helper, StringComparison.Ordinal);
Assert.Contains("nextRow.scrollIntoView({ block: \"nearest\", inline: \"nearest\" });",
helper,
StringComparison.Ordinal);
Assert.True(clickStart >= 0 && changeStart > clickStart);
Assert.Contains(
"button[data-overseas-action-id]",
App[clickStart..changeStart],
Assert.Contains("const overseasList = event.target.closest(\"[data-overseas-list]\");",
keyDown,
StringComparison.Ordinal);
Assert.DoesNotContain("NAS@IXIC", App, StringComparison.Ordinal);
Assert.DoesNotContain("FOREIGN_INDEX", App, StringComparison.Ordinal);
Assert.DoesNotContain("index-candle-20d", App, StringComparison.Ordinal);
Assert.Contains("event.stopPropagation();", keyDown, StringComparison.Ordinal);
Assert.Contains("moveOverseasListSelection(overseasList, event.key);", keyDown,
StringComparison.Ordinal);
Assert.Contains("button.tabIndex = -1;", App, StringComparison.Ordinal);
Assert.Contains("fixedList.tabIndex = 0;", App, StringComparison.Ordinal);
Assert.Contains("list.tabIndex = 0;", App, StringComparison.Ordinal);
}
private static string Section(string startToken, string endToken)
{
var start = App.IndexOf(startToken, StringComparison.Ordinal);
var end = App.IndexOf(endToken, start + startToken.Length, StringComparison.Ordinal);
Assert.True(start >= 0 && end > start, $"Unable to locate {startToken} section.");
return App[start..end];
}
private static string FindRepositoryRoot()

View File

@@ -72,6 +72,177 @@ public sealed class LegacyPlayoutNativeContractTests
Assert.Contains("WaitForPlayCompletionAsync", Playout, StringComparison.Ordinal);
}
[Fact]
public void AutomaticRefreshUsesTheCoreSchedulerAndGenerationOwnedState()
{
Assert.Contains("new LegacyRefreshScheduler(", Playout, StringComparison.Ordinal);
Assert.Contains(
"LegacyRefreshEpoch<LegacyRefreshRuntimeStatus>",
Playout,
StringComparison.Ordinal);
Assert.Contains("_refreshEpoch.ReadState()", Playout, StringComparison.Ordinal);
Assert.Contains("_refreshEpoch.Replace(", Playout, StringComparison.Ordinal);
Assert.Contains("_refreshEpoch.TryUpdate(", Playout, StringComparison.Ordinal);
Assert.Contains("_refreshEpoch.TryComplete(", Playout, StringComparison.Ordinal);
Assert.DoesNotContain("Task.Delay(", Playout, StringComparison.Ordinal);
foreach (var obsoleteRawField in new[]
{
"_refreshCancellation",
"_refreshCts",
"_refreshStateGate",
"_refreshActive",
"_refreshCompletedCount",
"_refreshMaximumCount",
"_refreshLimitReached",
"_refreshNextAtUtc",
"_refreshLastSuccessAtUtc",
"_refreshFaultCode",
"_refreshMessage"
})
{
Assert.DoesNotContain(obsoleteRawField, Playout, StringComparison.Ordinal);
}
}
[Fact]
public void ZeroRefreshLimitReturnsBeforeCreatingAnEpochOrLoop()
{
var start = Slice(
Playout,
"private void StartRefreshLoop(LegacyPlayoutWorkflow workflow)",
"private void StopRefreshLoop()");
var limitCheck = RequiredIndex(start, "if (scheduler.HasReachedMaximum)");
var linkedEpoch = RequiredIndex(
start,
"CancellationTokenSource.CreateLinkedTokenSource(");
var loopAssignment = RequiredIndex(
start,
"_refreshTask = RunRefreshLoopAsync(workflow, scheduler, cancellation);");
Assert.True(limitCheck < linkedEpoch);
Assert.True(linkedEpoch < loopAssignment);
var zeroLimitBranch = start[limitCheck..linkedEpoch];
Assert.Contains("_refreshTask = Task.CompletedTask;", zeroLimitBranch,
StringComparison.Ordinal);
Assert.Contains("_refreshEpoch.Stop(", zeroLimitBranch, StringComparison.Ordinal);
Assert.Contains("return;", zeroLimitBranch, StringComparison.Ordinal);
Assert.DoesNotContain("RunRefreshLoopAsync(", zeroLimitBranch,
StringComparison.Ordinal);
}
[Fact]
public void AutomaticRefreshSerializesDbThenPlayoutAndPublishesBothBusyStates()
{
var loop = Slice(
Playout,
"private async Task RunRefreshLoopAsync(",
"private bool TrySetRefreshFault(");
var intentWait = RequiredIndex(loop, "await _intentGate.WaitAsync(token)");
var intentBusy = RequiredIndex(loop, "_intentBusy = true;", intentWait);
var intentBusyState = RequiredIndex(loop, "QueueOperatorState();", intentBusy);
var playoutWait = RequiredIndex(
loop,
"await _playoutCommandGate.WaitAsync(token)",
intentBusyState);
var playoutBusy = RequiredIndex(
loop,
"Interlocked.Exchange(ref _playoutBusy, 1);",
playoutWait);
var playoutBusyState = RequiredIndex(loop, "QueueOperatorState();", playoutBusy);
var refreshDispatch = RequiredIndex(
loop,
"workflow.RefreshOnAirAsync",
playoutBusyState);
Assert.True(intentWait < intentBusy);
Assert.True(intentBusy < intentBusyState);
Assert.True(intentBusyState < playoutWait);
Assert.True(playoutWait < playoutBusy);
Assert.True(playoutBusy < playoutBusyState);
Assert.True(playoutBusyState < refreshDispatch);
}
[Fact]
public void KnownRefreshInputFailuresStopWithoutQuarantineButUnknownsDoQuarantine()
{
var loop = Normalize(Slice(
Playout,
"private async Task RunRefreshLoopAsync(",
"private bool TrySetRefreshFault("));
var databaseFailure = Slice(
loop,
"catch (DatabaseInfrastructureException)",
"catch (LegacySceneDataException)");
var sceneDataFailure = Slice(
loop,
"catch (LegacySceneDataException)",
"catch (ArgumentException)");
var argumentFailure = Slice(
loop,
"catch (ArgumentException)",
"catch\n {");
var unknownFailure = Slice(
loop,
"catch\n {",
"finally");
AssertKnownFailureDoesNotQuarantine(
databaseFailure,
"DATABASE_UNAVAILABLE");
AssertKnownFailureDoesNotQuarantine(
sceneDataFailure,
"SCENE_DATA_INVALID");
AssertKnownFailureDoesNotQuarantine(
argumentFailure,
"SCENE_ARGUMENT_INVALID");
Assert.Contains("REFRESH_OUTCOME_UNKNOWN", unknownFailure,
StringComparison.Ordinal);
Assert.Contains("QuarantinePlayoutAsync", unknownFailure,
StringComparison.Ordinal);
Assert.Contains(
"result.Code is PlayoutResultCode.OutcomeUnknown or\n" +
" PlayoutResultCode.TimedOut",
loop,
StringComparison.Ordinal);
AssertEveryFaultBranchQuarantines(loop, "PLAY_CALLBACK_TIMEOUT");
}
[Fact]
public void ExternalIdleStatusResetsNormalRefreshState()
{
var handler = Slice(
Playout,
"private void OnPlayoutStatusChanged(",
"private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(");
Assert.Contains(
"string.IsNullOrWhiteSpace(args.Current.PreparedSceneName)",
handler,
StringComparison.Ordinal);
Assert.Contains(
"string.IsNullOrWhiteSpace(args.Current.OnAirSceneName)",
handler,
StringComparison.Ordinal);
Assert.Contains(
"args.Current.State == PlayoutConnectionState.OutcomeUnknown",
handler,
StringComparison.Ordinal);
var stop = RequiredIndex(handler, "StopRefreshLoop();");
var normalElse = RequiredIndex(handler, "else", stop);
var reset = RequiredIndex(handler, "ResetRefreshState();", normalElse);
Assert.True(stop < normalElse && normalElse < reset);
var resetHelper = Slice(
Playout,
"private void ResetRefreshState()",
"private async Task RunRefreshLoopAsync(");
Assert.Contains(
"_refreshEpoch.Stop(_ => LegacyRefreshRuntimeStatus.Empty",
resetHelper,
StringComparison.Ordinal);
}
[Fact]
public void UnknownOutcomeQuarantinesWithoutAutomaticCommandRetry()
{
@@ -143,6 +314,58 @@ public sealed class LegacyPlayoutNativeContractTests
StringComparison.Ordinal);
}
private static void AssertKnownFailureDoesNotQuarantine(
string failureBlock,
string expectedFaultCode)
{
Assert.Contains(expectedFaultCode, failureBlock, StringComparison.Ordinal);
Assert.DoesNotContain(
"QuarantinePlayoutAsync",
failureBlock,
StringComparison.Ordinal);
}
private static void AssertEveryFaultBranchQuarantines(
string source,
string faultCode)
{
var occurrence = 0;
var count = 0;
while ((occurrence = source.IndexOf(
faultCode,
occurrence,
StringComparison.Ordinal)) >= 0)
{
var branchEnd = source.IndexOf("return;", occurrence, StringComparison.Ordinal);
Assert.True(branchEnd > occurrence, $"{faultCode} branch has no return.");
Assert.Contains(
"QuarantinePlayoutAsync",
source[occurrence..branchEnd],
StringComparison.Ordinal);
count++;
occurrence = branchEnd + "return;".Length;
}
Assert.True(count > 0, $"No {faultCode} branch was found.");
}
private static string Slice(string source, string startMarker, string endMarker)
{
var start = RequiredIndex(source, startMarker);
var end = RequiredIndex(source, endMarker, start + startMarker.Length);
return source[start..end];
}
private static int RequiredIndex(string source, string marker, int startIndex = 0)
{
var index = source.IndexOf(marker, startIndex, StringComparison.Ordinal);
Assert.True(index >= 0, $"Required source marker was not found: {marker}");
return index;
}
private static string Normalize(string source) =>
source.Replace("\r\n", "\n", StringComparison.Ordinal);
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);

View File

@@ -33,21 +33,34 @@ public sealed class LegacyRowDoubleClickParityTests
}
[Fact]
public void Theme_double_click_is_atomic_while_single_click_remains_delayed_selection()
public void Theme_double_click_is_atomic_while_single_click_selects_immediately()
{
var doubleClick = Extract(
"categoryTree.addEventListener(\"dblclick\"",
"categoryTree.addEventListener(\"click\"");
var click = Extract(
var clickDelegate = Extract(
"categoryTree.addEventListener(\"click\"",
"categoryTree.addEventListener(\"change\"");
var themeClickStart = clickDelegate.IndexOf(
"const themeResult = event.target.closest(\"button[data-theme-result-id]\");",
StringComparison.Ordinal);
var themeClickEnd = clickDelegate.IndexOf(
"if (event.target.closest(\"button[data-theme-search]\"))",
themeClickStart,
StringComparison.Ordinal);
Assert.True(themeClickStart >= 0 && themeClickEnd > themeClickStart);
var themeClick = clickDelegate[themeClickStart..themeClickEnd];
Assert.Contains("send(\"activate-theme-result\"", doubleClick,
StringComparison.Ordinal);
Assert.DoesNotContain("send(\"select-theme\"", doubleClick,
StringComparison.Ordinal);
Assert.Contains("window.setTimeout", click, StringComparison.Ordinal);
Assert.Contains("send(\"select-theme\"", click, StringComparison.Ordinal);
Assert.Contains(
"send(\"select-theme\", { resultId: themeResult.dataset.themeResultId });",
themeClick,
StringComparison.Ordinal);
Assert.DoesNotContain("window.setTimeout", themeClick, StringComparison.Ordinal);
Assert.DoesNotContain("themeResultClickTimer", App, StringComparison.Ordinal);
}
[Fact]

View File

@@ -39,8 +39,9 @@ public sealed class LegacySceneActionClickParityTests
var doubleClickDelegate = ExtractDelegate("dblclick", "click");
var clickDelegate = ExtractDelegate("click", "change");
Assert.Contains("send(\"double-click-industry\"", doubleClickDelegate,
Assert.Contains("activateIndustryRow(industryRow)", doubleClickDelegate,
StringComparison.Ordinal);
Assert.Contains("send(\"double-click-industry\"", App, StringComparison.Ordinal);
Assert.Contains("send(\"activate-comparison-action\"", doubleClickDelegate,
StringComparison.Ordinal);
Assert.Contains("send(\"select-theme\"", clickDelegate, StringComparison.Ordinal);

View File

@@ -0,0 +1,172 @@
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("document.activeElement !== input", 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.");
}
}

View File

@@ -154,18 +154,50 @@ public sealed class LegacyWebContractTests
StringComparison.Ordinal);
Assert.DoesNotContain("send(\"toggle-active-playlist-enabled\"", App,
StringComparison.Ordinal);
Assert.Contains("event.target.closest(\"input, button\")", App,
StringComparison.Ordinal);
Assert.Contains("deferSelectedRowPlainClickForDrag", App, StringComparison.Ordinal);
Assert.Contains("deferredPlaylistPlainSelectionRowId", App, StringComparison.Ordinal);
Assert.Contains("event.dataTransfer.setData(playlistDragMime, rowId)", App,
Assert.Contains("rowHeader.className = \"playlist-row-header\"", App,
StringComparison.Ordinal);
Assert.Contains("event.stopPropagation()", App, StringComparison.Ordinal);
Assert.Contains("send(\"activate-playlist-row\", { rowId: element.dataset.rowId })", App,
StringComparison.Ordinal);
Assert.Contains("rowHeader.textContent = String(position + 1)", App,
StringComparison.Ordinal);
Assert.Contains("beginPlaylistDragGesture(rowHeader, element, event)", App,
StringComparison.Ordinal);
Assert.Contains("rowHeader.addEventListener(\"pointermove\", onPlaylistPointerMove)", App,
StringComparison.Ordinal);
Assert.Contains("handle.setPointerCapture(event.pointerId)", App,
StringComparison.Ordinal);
Assert.Contains("window.LegacyCutDrag.createDragRectangle(position, cutDragSize)", App,
StringComparison.Ordinal);
Assert.Contains("gesture.rectangle, playlistPointerPosition(event)", App,
StringComparison.Ordinal);
Assert.Contains("pointed.closest(\".playlist-row-header\")", App,
StringComparison.Ordinal);
Assert.Contains("targetHeader.closest(\".playlist-data-row\")", App,
StringComparison.Ordinal);
Assert.DoesNotContain("playlistDragMime", App, StringComparison.Ordinal);
Assert.DoesNotContain("event.dataTransfer.setData(playlistDragMime", App,
StringComparison.Ordinal);
Assert.DoesNotContain("selectedRowIds", App, StringComparison.Ordinal);
Assert.Contains("element.draggable = false", App, StringComparison.Ordinal);
Assert.Contains("enabled.draggable = false", App, StringComparison.Ordinal);
Assert.Contains("beginCutDragGesture(element, event, position)", App,
StringComparison.Ordinal);
Assert.Contains("window.LegacyCutDrag.shouldStart", App, StringComparison.Ordinal);
Assert.Contains("if (playlistMutationLocked || !rowId || !event.dataTransfer", App,
Assert.Contains("(playlistDragGesture && playlistDragGesture.started)", App,
StringComparison.Ordinal);
Assert.Contains("const isDragSource = playlistDragGesture?.started === true", App,
StringComparison.Ordinal);
Assert.Contains("dragTargetPosition === \"after\"", App,
StringComparison.Ordinal);
Assert.Contains("clearPlaylistDragGesture()", App, StringComparison.Ordinal);
Assert.Contains("<div aria-hidden=\"true\"></div><div>송출</div>", Markup,
StringComparison.Ordinal);
Assert.Contains("grid-template-columns: 30px 40px 100px", Styles,
StringComparison.Ordinal);
Assert.Contains(".playlist-data-row .playlist-row-header", Styles,
StringComparison.Ordinal);
Assert.Contains("if (!playlistMutationLocked) send(\"delete-selected-playlist-rows\"", App,
StringComparison.Ordinal);
@@ -279,6 +311,8 @@ public sealed class LegacyWebContractTests
Assert.Contains("send(\"double-click-industry\"", App, StringComparison.Ordinal);
Assert.Contains("send(\"swap-industries\"", App, StringComparison.Ordinal);
Assert.Contains("send(\"activate-industry-action\"", App, StringComparison.Ordinal);
Assert.DoesNotContain("swap.textContent = \"↕ 교환\"", App,
StringComparison.Ordinal);
}
[Fact]
@@ -311,6 +345,18 @@ public sealed class LegacyWebContractTests
Assert.Contains("dataset.comparisonPairId", App, StringComparison.Ordinal);
Assert.Contains("dataset.comparisonActionId", App, StringComparison.Ordinal);
Assert.Contains("comparisonTargetClickTimer", App, StringComparison.Ordinal);
foreach (var originalLabel in new[] { "◀▶", "추가", "UP", "DOWN", "DEL", "DEL ALL" })
{
Assert.Contains($"\"{originalLabel}\"", App, StringComparison.Ordinal);
}
Assert.DoesNotContain("clear.dataset.comparisonClear", App, StringComparison.Ordinal);
Assert.DoesNotContain(
"window.confirm(\"저장된 종목 비교쌍을 모두 삭제하겠습니까?\")",
App,
StringComparison.Ordinal);
Assert.Contains("? \"confirm-native-dialog\"", App, StringComparison.Ordinal);
Assert.Contains("send(\"cancel-native-dialog\", {});", App,
StringComparison.Ordinal);
Assert.DoesNotContain("stockCode", App, StringComparison.Ordinal);
}