feat: advance legacy UI behavior parity
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
})
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user