feat: complete legacy UI and playout parity migration
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyCloseConfirmationNativeContractTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string AppRoot = Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp");
|
||||
private static readonly string Window = File.ReadAllText(
|
||||
Path.Combine(AppRoot, "MainWindow.xaml.cs"));
|
||||
private static readonly string Confirmation = File.ReadAllText(
|
||||
Path.Combine(AppRoot, "MainWindow.CloseConfirmation.cs"));
|
||||
|
||||
[Fact]
|
||||
public void SystemCloseRequiresExplicitDefaultCancelConfirmation()
|
||||
{
|
||||
Assert.Contains("EnsureCloseConfirmationAttached();", Window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("appWindow.Closing += OnAppWindowClosing", Confirmation,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("args.Cancel = true", Confirmation, StringComparison.Ordinal);
|
||||
Assert.Contains("DefaultButton = ContentDialogButton.Close", Confirmation,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("PrimaryButtonText = \"종료\"", Confirmation,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("CloseButtonText = \"취소\"", Confirmation,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("Interlocked.Exchange(ref _closeConfirmed, 1);", Confirmation,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("DetachCloseConfirmation();", Window, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClosingNeverInventsAnAutomaticPlayoutCommand()
|
||||
{
|
||||
Assert.Contains("종료 과정에서는 TAKE OUT을 자동 실행하지 않습니다", Confirmation,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("TakeOutAsync", Confirmation, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("StopAll", Confirmation, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ExecuteOperatorPlayoutAsync", Confirmation,
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyFixedSectionBatchWebTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string App = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web",
|
||||
"app.js"));
|
||||
private static readonly string MainWindow = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"MainWindow.xaml.cs"));
|
||||
private static readonly string Playout = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"MainWindow.Playout.cs"));
|
||||
|
||||
[Fact]
|
||||
public void Fixed_section_progress_is_native_state_and_locks_other_catalog_actions()
|
||||
{
|
||||
var fixedCatalog = Extract("function renderFixedCatalog", "function renderIndustry");
|
||||
|
||||
Assert.Contains("const batch = state.fixedSectionBatch", fixedCatalog,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("button.disabled = state.isBusy === true || batch != null",
|
||||
fixedCatalog,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("summary.setAttribute(\"aria-disabled\"", fixedCatalog,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("section.getAttribute(\"aria-disabled\") !== \"true\"", App,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Manual_modal_explains_atomic_staging_and_exposes_only_confirm_next_or_cancel()
|
||||
{
|
||||
var manual = Extract("function renderManualLists", "function openNamedPlaylist");
|
||||
|
||||
Assert.Contains("const fixedBatch = state.fixedSectionBatch", manual,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("아직 편성표에 추가되지 않았습니다", manual, StringComparison.Ordinal);
|
||||
Assert.Contains("button.disabled = busy || fixedBatch != null", manual,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("확인 후 다음", manual, StringComparison.Ordinal);
|
||||
Assert.Contains("확인 후 섹션 추가", manual, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"confirm-manual-list\", {})", manual, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("resume-fixed-section", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Native_dispatch_awaits_the_whole_fixed_section_workflow()
|
||||
{
|
||||
Assert.Contains("await _controller.ActivateFixedSection(", MainWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("section.SectionIndex", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("cancellationToken", MainWindow, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Native_boundary_allows_only_current_manual_edits_until_batch_completion()
|
||||
{
|
||||
var allowlist = Extract(
|
||||
Playout,
|
||||
"private static bool IsFixedSectionBatchMutationIntent",
|
||||
"private void ObserveOperatorMutationQuarantine");
|
||||
Assert.Contains("_controller.Current.FixedSectionBatch is not null", MainWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("!IsFixedSectionBatchMutationIntent(intent)", MainWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacySetManualNetSellCellIntent", allowlist, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyConfirmManualListIntent", allowlist, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacyDropSelectedCutsIntent", allowlist, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string Extract(string startToken, string endToken)
|
||||
{
|
||||
return Extract(App, startToken, endToken);
|
||||
}
|
||||
|
||||
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 && end > start);
|
||||
return source[start..end];
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyGraphClickParityWebTests
|
||||
{
|
||||
private static readonly string App = File.ReadAllText(Path.Combine(
|
||||
FindRepositoryRoot(),
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web",
|
||||
"app.js"));
|
||||
|
||||
[Fact]
|
||||
public void Uc3_targets_select_on_single_click_and_fill_slots_only_on_double_click()
|
||||
{
|
||||
var doubleStart = App.IndexOf(
|
||||
"categoryTree.addEventListener(\"dblclick\"",
|
||||
StringComparison.Ordinal);
|
||||
var clickStart = App.IndexOf(
|
||||
"categoryTree.addEventListener(\"click\"",
|
||||
doubleStart + 1,
|
||||
StringComparison.Ordinal);
|
||||
var changeStart = App.IndexOf(
|
||||
"categoryTree.addEventListener(\"change\"",
|
||||
clickStart + 1,
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(doubleStart >= 0 && clickStart > doubleStart && changeStart > clickStart);
|
||||
var doubleClick = App[doubleStart..clickStart];
|
||||
var singleClick = App[clickStart..changeStart];
|
||||
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"choose-comparison-domestic",
|
||||
"choose-comparison-world",
|
||||
"choose-comparison-fixed"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"\"{intent}\"", doubleClick, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain($"\"{intent}\"", singleClick, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"select-comparison-domestic",
|
||||
"select-comparison-world",
|
||||
"select-comparison-fixed"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"\"{intent}\"", singleClick, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain($"\"{intent}\"", doubleClick, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("comparisonTargetClickTimer", singleClick, StringComparison.Ordinal);
|
||||
Assert.Contains("}, 250);", singleClick, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vi_result_single_click_selects_and_double_click_adds_without_duplicate_intent()
|
||||
{
|
||||
var start = App.IndexOf("function renderManualLists", StringComparison.Ordinal);
|
||||
var end = App.IndexOf("function openNamedPlaylist", start, StringComparison.Ordinal);
|
||||
Assert.True(start >= 0 && end > start);
|
||||
var manual = App[start..end];
|
||||
|
||||
Assert.Matches(
|
||||
new Regex(
|
||||
"addEventListener\\(\"click\"[\\s\\S]*?setTimeout[\\s\\S]*?send\\(\"select-manual-vi-result\"",
|
||||
RegexOptions.CultureInvariant),
|
||||
manual);
|
||||
Assert.Matches(
|
||||
new Regex(
|
||||
"addEventListener\\(\"dblclick\"[\\s\\S]*?clearDelayedClick\\(manualViResultClickTimer\\)[\\s\\S]*?send\\(\"add-manual-vi-result\"",
|
||||
RegexOptions.CultureInvariant),
|
||||
manual);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyHiddenNonOriginalControlsTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string Markup = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web",
|
||||
"index.html"));
|
||||
private static readonly string Styles = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web",
|
||||
"styles.css"));
|
||||
|
||||
[Fact]
|
||||
public void Non_original_shortcuts_prepare_and_fade_are_hidden_but_remain_native_wired()
|
||||
{
|
||||
Assert.Contains("class=\"manual-list-actions\" aria-label=\"수동 송출 목록\" hidden",
|
||||
Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("id=\"playout-prepare\" class=\"prepare\" type=\"button\" disabled hidden",
|
||||
Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("class=\"fade-control\" for=\"playout-fade-duration\" hidden",
|
||||
Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("id=\"playout-fade-duration\" disabled aria-label=\"장면 효과 시간\" hidden",
|
||||
Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("*[hidden] { display: none !important; }", Styles,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
foreach (var id in new[]
|
||||
{
|
||||
"playout-prepare",
|
||||
"playout-fade-duration"
|
||||
})
|
||||
{
|
||||
Assert.Equal(1, Count(Markup, $"id=\"{id}\""));
|
||||
}
|
||||
|
||||
foreach (var screen in new[] { "individual", "foreign", "institution", "vi" })
|
||||
{
|
||||
Assert.Contains($"data-manual-list-screen=\"{screen}\"", Markup,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
private static int Count(string value, string token)
|
||||
{
|
||||
var count = 0;
|
||||
var start = 0;
|
||||
while ((start = value.IndexOf(token, start, StringComparison.Ordinal)) >= 0)
|
||||
{
|
||||
count++;
|
||||
start += token.Length;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyManualFinancialParityWebTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string WebRoot = Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web");
|
||||
private static readonly string Markup = File.ReadAllText(Path.Combine(WebRoot, "index.html"));
|
||||
private static readonly string App = File.ReadAllText(Path.Combine(WebRoot, "app.js"));
|
||||
private static readonly string Styles = File.ReadAllText(Path.Combine(WebRoot, "styles.css"));
|
||||
private static readonly string MainWindow = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"MainWindow.xaml.cs"));
|
||||
|
||||
[Theory]
|
||||
[InlineData("revenue-composition", "주요매출 구성")]
|
||||
[InlineData("growth-metrics", "성장성 지표")]
|
||||
[InlineData("sales", "매출액")]
|
||||
[InlineData("operating-profit", "영업이익")]
|
||||
public void Original_left_GraphE_buttons_are_enabled_closed_screen_intents(
|
||||
string screen,
|
||||
string label)
|
||||
{
|
||||
Assert.Matches(
|
||||
$"<button(?![^>]*disabled)[^>]*data-manual-screen=\"{screen}\"[^>]*>{label}</button>",
|
||||
Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Modal_exposes_list_stock_editor_CRUD_and_playlist_controls()
|
||||
{
|
||||
foreach (var id in new[]
|
||||
{
|
||||
"manual-financial-modal",
|
||||
"manual-financial-title",
|
||||
"manual-financial-close",
|
||||
"manual-financial-workspace"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"id=\"{id}\"", Markup, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"open-manual-financial",
|
||||
"close-manual-financial",
|
||||
"search-manual-financial",
|
||||
"find-manual-financial",
|
||||
"toggle-manual-financial-name-sort",
|
||||
"load-manual-financial",
|
||||
"begin-manual-financial-create",
|
||||
"search-manual-financial-stocks",
|
||||
"select-manual-financial-stock",
|
||||
"save-manual-financial",
|
||||
"delete-manual-financial",
|
||||
"delete-all-manual-financial",
|
||||
"activate-manual-financial-action"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"\"{intent}\"", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains(".manual-financial-dialog", Styles, StringComparison.Ordinal);
|
||||
Assert.Contains(".manual-financial-grid", Styles, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphE_find_and_name_header_sort_are_generation_bound_native_intents()
|
||||
{
|
||||
Assert.Contains("manual-financial-find-query", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.manualFindDirection", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.manualNameSort", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.manualGeneration", App, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"find-manual-financial\"", App, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"send(\"toggle-manual-financial-name-sort\"",
|
||||
App,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("generation: generation", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Manual_Web_section_sends_values_and_opaque_ids_without_DB_or_scene_authority()
|
||||
{
|
||||
var start = App.IndexOf("function blankManualRecord", StringComparison.Ordinal);
|
||||
var end = App.IndexOf("function renderDialog", start, StringComparison.Ordinal);
|
||||
Assert.True(start >= 0 && end > start);
|
||||
var manualSection = App[start..end];
|
||||
|
||||
Assert.DoesNotMatch(
|
||||
new Regex("(?:INPUT_PIE|INPUT_GROW|INPUT_SELL|INPUT_PROFIT|s5076|s5079|s5080|s5081|rowVersion|builderKey|cutCode|dataCode|confirmationToken)"),
|
||||
manualSection);
|
||||
Assert.Contains("dataset.manualResultId", manualSection, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.manualStockResultId", manualSection, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.manualActionId", manualSection, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MainWindow_composes_real_read_and_one_shot_write_services_and_busy_dispatch()
|
||||
{
|
||||
Assert.Contains("new LegacyManualFinancialScreenService(", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("new OracleManualFinancialMutationExecutor(", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("new LegacyStockSearchService(", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("manualFinancialWorkflow: new LegacyManualFinancialWorkflow(", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacySaveManualFinancialIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyDeleteManualFinancialIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyDeleteAllManualFinancialIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("_intentGate.WaitAsync(", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("0,\n _lifetimeCancellation.Token", MainWindow, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("SaveManualFinancialAsync(", MainWindow[..MainWindow.IndexOf(
|
||||
"HandleIntentAsync",
|
||||
StringComparison.Ordinal)], StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyOperatorCatalogParityWebTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string WebRoot = Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web");
|
||||
private static readonly string Markup = File.ReadAllText(Path.Combine(WebRoot, "index.html"));
|
||||
private static readonly string App = File.ReadAllText(Path.Combine(WebRoot, "app.js"));
|
||||
private static readonly string Styles = File.ReadAllText(Path.Combine(WebRoot, "styles.css"));
|
||||
private static readonly string MainWindow = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"MainWindow.xaml.cs"));
|
||||
private static readonly string Playout = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"MainWindow.Playout.cs"));
|
||||
|
||||
[Fact]
|
||||
public void Theme_and_expert_workspaces_expose_the_original_management_entry_points()
|
||||
{
|
||||
foreach (var label in new[]
|
||||
{
|
||||
"테마검색", "테마추가", "테마편집", "테마삭제",
|
||||
"전문가 추가", "전문가 삭제", "추천행 편집·저장·삭제"
|
||||
})
|
||||
{
|
||||
Assert.Contains(label, App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"begin-uc4-theme-catalog",
|
||||
"edit-uc4-selected-theme",
|
||||
"delete-uc4-selected-theme",
|
||||
"begin-uc6-expert-catalog",
|
||||
"edit-uc6-selected-expert",
|
||||
"delete-uc6-selected-expert"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"send(\"{intent}\", {{}})", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.DoesNotContain("ThemeA 추가·편집·삭제", App, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("EList 추가·편집·삭제", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Modal_contains_search_create_edit_delete_stock_and_reorder_controls()
|
||||
{
|
||||
foreach (var id in new[]
|
||||
{
|
||||
"operator-catalog-modal",
|
||||
"operator-catalog-query",
|
||||
"operator-catalog-new",
|
||||
"operator-catalog-results",
|
||||
"operator-catalog-name",
|
||||
"operator-catalog-stock-query",
|
||||
"operator-catalog-stock-results",
|
||||
"operator-catalog-add-stock",
|
||||
"operator-catalog-draft-rows",
|
||||
"operator-catalog-save",
|
||||
"operator-catalog-delete"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"id=\"{id}\"", Markup, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("operator-catalog-draft-row", Styles, StringComparison.Ordinal);
|
||||
Assert.Contains("operator-catalog-backdrop[hidden]", Styles, StringComparison.Ordinal);
|
||||
Assert.Contains("operatorCatalogStockResults.addEventListener(\"dblclick\"", App,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.operatorCatalogBuyAmountRowId = row.rowId", App,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("remove.textContent = isTheme ? \"삭제\" : \"종목삭제\"", App,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Web_sends_closed_intents_and_never_owns_catalog_DB_identity()
|
||||
{
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"open-operator-catalog",
|
||||
"search-operator-catalog",
|
||||
"select-operator-catalog-result",
|
||||
"begin-create-theme-catalog",
|
||||
"begin-create-expert-catalog",
|
||||
"search-operator-catalog-stocks",
|
||||
"select-operator-catalog-stock",
|
||||
"add-operator-catalog-stock",
|
||||
"activate-operator-catalog-stock",
|
||||
"move-operator-catalog-draft-row",
|
||||
"remove-operator-catalog-draft-row",
|
||||
"set-operator-catalog-draft-buy-amount",
|
||||
"save-operator-catalog",
|
||||
"delete-operator-catalog"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"\"{intent}\"", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
var start = App.IndexOf("function renderOperatorCatalog", StringComparison.Ordinal);
|
||||
var end = App.IndexOf("function renderDialog", start, StringComparison.Ordinal);
|
||||
Assert.True(start >= 0 && end > start);
|
||||
var catalogSection = App[start..end];
|
||||
Assert.DoesNotMatch(
|
||||
new Regex("(?:themeCode|expertCode|stockCode|itemCode|expectedIdentity|SB_LIST|EXPERT_LIST)"),
|
||||
catalogSection);
|
||||
Assert.DoesNotContain("send(\"save-operator-catalog\"", App[..start], StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MainWindow_composes_the_non_retrying_real_services_and_dispatches_every_intent()
|
||||
{
|
||||
Assert.Contains("new OperatorCatalogMutationExecutor(", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("new LegacyThemeCatalogPersistenceService(", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("new LegacyExpertCatalogPersistenceService(", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new LegacyStockMasterIdentityValidationService(",
|
||||
MainWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new LegacyOperatorCatalogSchemaValidationService(",
|
||||
MainWindow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyOpenOperatorCatalogIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyBeginUc4ThemeCatalogIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyEditUc4SelectedThemeIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyDeleteUc4SelectedThemeIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyBeginUc6ExpertCatalogIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyEditUc6SelectedExpertIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyDeleteUc6SelectedExpertIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacySaveOperatorCatalogIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyDeleteOperatorCatalogIntent", MainWindow, StringComparison.Ordinal);
|
||||
Assert.Contains("_intentGate.WaitAsync(", MainWindow, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Playout_guard_blocks_catalog_draft_and_DB_mutations_without_weakening_existing_guards()
|
||||
{
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"LegacyBeginCreateThemeCatalogIntent",
|
||||
"LegacyBeginCreateExpertCatalogIntent",
|
||||
"LegacyBeginUc4ThemeCatalogIntent",
|
||||
"LegacyEditUc4SelectedThemeIntent",
|
||||
"LegacyDeleteUc4SelectedThemeIntent",
|
||||
"LegacyBeginUc6ExpertCatalogIntent",
|
||||
"LegacyEditUc6SelectedExpertIntent",
|
||||
"LegacyDeleteUc6SelectedExpertIntent",
|
||||
"LegacyAddOperatorCatalogStockIntent",
|
||||
"LegacyActivateOperatorCatalogStockIntent",
|
||||
"LegacyMoveOperatorCatalogDraftRowIntent",
|
||||
"LegacyRemoveOperatorCatalogDraftRowIntent",
|
||||
"LegacySetOperatorCatalogDraftBuyAmountIntent",
|
||||
"LegacySaveOperatorCatalogIntent",
|
||||
"LegacyDeleteOperatorCatalogIntent",
|
||||
"LegacyActivateThemeActionIntent",
|
||||
"LegacyActivateExpertActionIntent"
|
||||
})
|
||||
{
|
||||
Assert.Contains(intent, Playout, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("snapshot.OperatorCatalog?.WritesQuarantined", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("status.IsPlayCompletionPending", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("workflow.PreparedCutCode", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("workflow.OnAirCutCode", Playout, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyOverseasParityWebTests
|
||||
{
|
||||
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 Markup = File.ReadAllText(Path.Combine(WebRoot, "index.html"));
|
||||
private static readonly string Styles = File.ReadAllText(Path.Combine(WebRoot, "styles.css"));
|
||||
|
||||
[Fact]
|
||||
public void OverseasTabRendersFixedIndustryStockAndActionWorkspaces()
|
||||
{
|
||||
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);
|
||||
|
||||
foreach (var selector in new[]
|
||||
{
|
||||
".overseas-workspace",
|
||||
".overseas-fixed-list",
|
||||
".overseas-searches",
|
||||
".overseas-results",
|
||||
".overseas-action-panel"
|
||||
})
|
||||
{
|
||||
Assert.Contains(selector, Styles, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverseasEventsSendOnlyQueriesAndOpaqueIds()
|
||||
{
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"select-overseas-fixed-index",
|
||||
"search-overseas-industries",
|
||||
"search-overseas-stocks",
|
||||
"select-overseas-industry",
|
||||
"select-overseas-stock",
|
||||
"activate-overseas-action"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"\"{intent}\"", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("data-overseas-action-id", App, StringComparison.Ordinal);
|
||||
Assert.Contains("event.key === \"Enter\"", App, StringComparison.Ordinal);
|
||||
var clickStart = App.IndexOf(
|
||||
"categoryTree.addEventListener(\"click\"",
|
||||
StringComparison.Ordinal);
|
||||
var changeStart = App.IndexOf(
|
||||
"categoryTree.addEventListener(\"change\"",
|
||||
clickStart,
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(clickStart >= 0 && changeStart > clickStart);
|
||||
Assert.Contains(
|
||||
"button[data-overseas-action-id]",
|
||||
App[clickStart..changeStart],
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("NAS@IXIC", App, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("FOREIGN_INDEX", App, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("index-candle-20d", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(current.FullName, "src")) &&
|
||||
Directory.Exists(Path.Combine(current.FullName, "tests")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Repository root was not found.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyPlayoutNativeContractTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string AppRoot = Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp");
|
||||
private static readonly string Playout = File.ReadAllText(
|
||||
Path.Combine(AppRoot, "MainWindow.Playout.cs"));
|
||||
private static readonly string Window = File.ReadAllText(
|
||||
Path.Combine(AppRoot, "MainWindow.xaml.cs"));
|
||||
private static readonly string Project = File.ReadAllText(
|
||||
Path.Combine(AppRoot, "MBN_STOCK_WEBVIEW.LegacyParityApp.csproj"));
|
||||
|
||||
[Fact]
|
||||
public void ParityAppComposesTheVerifiedThirtyFiveSceneRuntime()
|
||||
{
|
||||
Assert.Contains("LegacySceneRuntimeFactory.cs", Project, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacySceneRuntimeFactory.Create(", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("trustedManualSource: _manualNetSellStore", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("trustedViSource: _manualViStore", Playout,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartupDefaultBackgroundDependsOnAbsentLocalConfigAndSurfacesWarning()
|
||||
{
|
||||
Assert.Contains("File.Exists(", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("PlayoutOptionsLoader.DefaultPath", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyParityStartupCompositionResolver.Resolve(", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_playoutInitializationWarning ?? status?.Message", Playout,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OperatorCommandsUseTheNativeWorkflowAndCSharpPlaylist()
|
||||
{
|
||||
Assert.Contains("_controller.CreatePlayoutRequest()", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("workflow.PrepareAsync(", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("workflow.PrepareAndTakeInAsync(", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("workflow.TakeInAsync(", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("workflow.NextAsync(", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("workflow.TakeOutAsync(", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyWorkflowNextKind.PlaylistNext", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacySceneRefreshPolicy.TryGetInterval", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("WaitForPlayCompletionAsync", Playout, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownOutcomeQuarantinesWithoutAutomaticCommandRetry()
|
||||
{
|
||||
Assert.Contains("PlayoutResultCode.OutcomeUnknown", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("PlayoutResultCode.TimedOut", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("QuarantinePlayoutAsync", Playout, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("MaximumReconnectAttempts", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ReconnectAsync", Playout, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedProgramAndUnknownWriteOutcomesBlockOperatorMutations()
|
||||
{
|
||||
Assert.Contains("CanAcceptOperatorMutation()", Window, StringComparison.Ordinal);
|
||||
Assert.Contains("IsOperatorMutationQuarantined()", Window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("ObserveOperatorMutationQuarantine(state)", Window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("ManualFinancial?.WritesQuarantined", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("NamedPlaylist?.IsWriteQuarantined", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("ManualLists?.WritesQuarantined", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("OperatorCatalog?.WritesQuarantined", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyConfirmManualListIntent", Playout,
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyPlayoutWebContractTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string WebRoot = Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web");
|
||||
private static readonly string Markup = File.ReadAllText(
|
||||
Path.Combine(WebRoot, "index.html"));
|
||||
private static readonly string Script = File.ReadAllText(
|
||||
Path.Combine(WebRoot, "playout-ui.js"));
|
||||
|
||||
[Fact]
|
||||
public void ShellExposesOriginalPlayoutBackgroundAndFadeControls()
|
||||
{
|
||||
foreach (var id in new[]
|
||||
{
|
||||
"playout-connection-state",
|
||||
"playout-background-none",
|
||||
"playout-background-file",
|
||||
"playout-background-name",
|
||||
"playout-fade-duration",
|
||||
"playout-prepare",
|
||||
"playout-take-in",
|
||||
"playout-next",
|
||||
"playout-take-out",
|
||||
"playout-status"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"id=\"{id}\"", Markup, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("PREPARE", Markup, StringComparison.Ordinal);
|
||||
Assert.Contains("TAKE IN", Markup, StringComparison.Ordinal);
|
||||
Assert.Contains("NEXT", Markup, StringComparison.Ordinal);
|
||||
Assert.Contains("TAKE OUT", Markup, StringComparison.Ordinal);
|
||||
var fadeMarkup = ExtractElement(Markup, "playout-fade-duration", "</select>");
|
||||
Assert.Equal(20, CountOccurrences(fadeMarkup, "<option value="));
|
||||
Assert.Contains("<script src=\"playout-ui.js\"></script>", Markup,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayoutWebLayerSendsCommandIntentOnly()
|
||||
{
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"prepare-playout",
|
||||
"take-in",
|
||||
"next-playout",
|
||||
"take-out",
|
||||
"choose-background",
|
||||
"toggle-background",
|
||||
"set-fade-duration"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"\"{intent}\"", Script, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.DoesNotContain("fetch(", Script, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("localStorage", Script, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("sessionStorage", Script, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("filePath", Script, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("sceneCode", Script, StringComparison.Ordinal);
|
||||
Assert.Contains("post(\"prepare-playout\", {})", Script,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("post(\"take-in\", {})", Script, StringComparison.Ordinal);
|
||||
Assert.Contains("post(\"next-playout\", {})", Script, StringComparison.Ordinal);
|
||||
Assert.Contains("post(\"take-out\", {})", Script, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OriginalGlobalShortcutsAreIsolatedFromOpenDialogs()
|
||||
{
|
||||
Assert.Contains("hasOpenDialog()", Script, StringComparison.Ordinal);
|
||||
Assert.Contains("event.key === \"F2\"", Script, StringComparison.Ordinal);
|
||||
Assert.Contains("event.key === \"F3\"", Script, StringComparison.Ordinal);
|
||||
Assert.Contains("event.key === \"F8\"", Script, StringComparison.Ordinal);
|
||||
Assert.Contains("event.key === \"Escape\"", Script, StringComparison.Ordinal);
|
||||
Assert.Contains("event.repeat || hasOpenDialog()", Script,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ButtonsFollowNativePhaseCallbackAndOutcomeState()
|
||||
{
|
||||
Assert.Contains("playout.outcomeUnknown !== true", Script,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("const canPrepareToggle = playout.phase === \"idle\"", Script,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("const canTakeIn = playout.phase === \"prepared\"", Script,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("playout.phase === \"idle\" && hasPlaylist", Script,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("playout.phase !== \"program\"", Script,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("playout.isPlayCompletionPending === true", Script,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("playout.nextKind === \"none\"", Script,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string source, string value)
|
||||
{
|
||||
var count = 0;
|
||||
var offset = 0;
|
||||
while ((offset = source.IndexOf(value, offset, StringComparison.Ordinal)) >= 0)
|
||||
{
|
||||
count++;
|
||||
offset += value.Length;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static string ExtractElement(string source, string id, string closingTag)
|
||||
{
|
||||
var start = source.IndexOf($"id=\"{id}\"", StringComparison.Ordinal);
|
||||
Assert.True(start >= 0, $"Element '{id}' was not found.");
|
||||
var end = source.IndexOf(closingTag, start, StringComparison.Ordinal);
|
||||
Assert.True(end >= 0, $"Closing tag for '{id}' was not found.");
|
||||
return source[start..(end + closingTag.Length)];
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Repository root was not found.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyRowDoubleClickParityTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string App = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web",
|
||||
"app.js"));
|
||||
private static readonly string MainWindow = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"MainWindow.xaml.cs"));
|
||||
|
||||
[Fact]
|
||||
public void GraphE_double_click_sends_one_opaque_atomic_command()
|
||||
{
|
||||
var handler = Extract(
|
||||
"manualWorkspace.addEventListener(\"dblclick\"",
|
||||
"manualWorkspace.addEventListener(\"click\"");
|
||||
|
||||
Assert.Contains("send(\"activate-manual-financial-result\"", handler,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("resultId: row.dataset.manualResultId", handler,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("load-manual-financial", handler, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("actionId", handler, StringComparison.Ordinal);
|
||||
Assert.Contains("clearDelayedClick(manualResultClickTimer)", handler,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Theme_double_click_is_atomic_while_single_click_remains_delayed_selection()
|
||||
{
|
||||
var doubleClick = Extract(
|
||||
"categoryTree.addEventListener(\"dblclick\"",
|
||||
"categoryTree.addEventListener(\"click\"");
|
||||
var click = Extract(
|
||||
"categoryTree.addEventListener(\"click\"",
|
||||
"categoryTree.addEventListener(\"change\"");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Named_playlist_double_click_uses_id_bound_commands_and_closes_after_receipt_only()
|
||||
{
|
||||
var doubleClick = Extract(
|
||||
"namedPlaylistDefinitions.addEventListener(\"dblclick\"",
|
||||
"namedPlaylistDefinitions.addEventListener(\"click\"");
|
||||
var render = Extract("function renderNamedPlaylist", "function renderOperatorCatalog");
|
||||
|
||||
Assert.Contains("\"load-named-playlist-by-id\"", doubleClick,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("\"save-current-named-playlist-to\"", doubleClick,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("window.confirm", doubleClick, StringComparison.Ordinal);
|
||||
Assert.Contains("definitionId: definitionId", doubleClick, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("select-named-playlist", doubleClick, StringComparison.Ordinal);
|
||||
Assert.Contains("commandResult.succeeded === true", render, StringComparison.Ordinal);
|
||||
Assert.Contains("commandResult.targetId === pendingNamedPlaylistCommand.definitionId",
|
||||
render,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("closeNamedPlaylist()", render, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Native_dispatch_awaits_all_atomic_read_then_materialize_commands()
|
||||
{
|
||||
foreach (var method in new[]
|
||||
{
|
||||
"ActivateManualFinancialResultAsync(",
|
||||
"ActivateThemeResultAsync(",
|
||||
"LoadNamedPlaylistByIdAsync(",
|
||||
"SaveCurrentNamedPlaylistToAsync("
|
||||
})
|
||||
{
|
||||
Assert.Contains(method, MainWindow, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Extract(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);
|
||||
return App[start..end];
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacySceneActionClickParityTests
|
||||
{
|
||||
private static readonly string App = File.ReadAllText(Path.Combine(
|
||||
FindRepositoryRoot(),
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web",
|
||||
"app.js"));
|
||||
|
||||
[Fact]
|
||||
public void ThemeExpertAndTradingHaltActions_AreSingleClickOnly()
|
||||
{
|
||||
// Original evidence: UC4.Designer.cs btn1..btn4, UC6.Designer.cs
|
||||
// btlistadd, and UC7.Designer.cs btn1/btn2 are wired to Click.
|
||||
var doubleClickDelegate = ExtractDelegate("dblclick", "click");
|
||||
var clickDelegate = ExtractDelegate("click", "change");
|
||||
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"activate-theme-action",
|
||||
"activate-expert-action",
|
||||
"activate-trading-halt-action"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"send(\"{intent}\"", clickDelegate, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain($"send(\"{intent}\"", doubleClickDelegate,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Single(Regex.Matches(App, Regex.Escape($"send(\"{intent}\"")).Cast<Match>());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExistingSelectionAndTrueDoubleClickBehavior_RemainsSeparated()
|
||||
{
|
||||
var doubleClickDelegate = ExtractDelegate("dblclick", "click");
|
||||
var clickDelegate = ExtractDelegate("click", "change");
|
||||
|
||||
Assert.Contains("send(\"double-click-industry\"", doubleClickDelegate,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"activate-comparison-action\"", doubleClickDelegate,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"select-theme\"", clickDelegate, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"select-expert\"", clickDelegate, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"select-trading-halt\"", clickDelegate,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string ExtractDelegate(string eventName, string nextEventName)
|
||||
{
|
||||
var startToken = $"categoryTree.addEventListener(\"{eventName}\"";
|
||||
var endToken = $"categoryTree.addEventListener(\"{nextEventName}\"";
|
||||
var start = App.IndexOf(startToken, StringComparison.Ordinal);
|
||||
var end = App.IndexOf(endToken, start + startToken.Length, StringComparison.Ordinal);
|
||||
Assert.True(start >= 0 && end > start);
|
||||
return App[start..end];
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(current.FullName, "src")) &&
|
||||
Directory.Exists(Path.Combine(current.FullName, "tests")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Repository root was not found.");
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,26 @@ public sealed class LegacyWebContractTests
|
||||
"stock-search",
|
||||
"stock-search-button",
|
||||
"stock-results",
|
||||
"market-tabs",
|
||||
"category-tree",
|
||||
"catalog-expand-all",
|
||||
"moving-average-5",
|
||||
"moving-average-20",
|
||||
"cut-list",
|
||||
"playlist-drop-zone",
|
||||
"playlist-rows",
|
||||
"playlist-move-up",
|
||||
"playlist-move-down",
|
||||
"playlist-delete-selected",
|
||||
"playlist-delete-all",
|
||||
"db-save-button",
|
||||
"db-load-button",
|
||||
"named-playlist-modal",
|
||||
"named-playlist-definitions",
|
||||
"named-playlist-new-title",
|
||||
"named-playlist-create-button",
|
||||
"named-playlist-delete-button",
|
||||
"named-playlist-confirm-button",
|
||||
"legacy-dialog"
|
||||
})
|
||||
{
|
||||
@@ -35,14 +52,8 @@ public sealed class LegacyWebContractTests
|
||||
Assert.Contains("TAKE OUT", Markup, StringComparison.Ordinal);
|
||||
Assert.Contains("NEXT", Markup, StringComparison.Ordinal);
|
||||
Assert.Matches("class=\"connection-state\"[^>]*>미연결</div>", Markup);
|
||||
Assert.Contains(
|
||||
"class=\"active\" type=\"button\" disabled>해외</button>",
|
||||
Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"type=\"button\" disabled>정지</button>",
|
||||
Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Matches("class=\"active\"[^>]*data-tab-id=\"overseas\">해외</button>", Markup);
|
||||
Assert.Matches("data-tab-id=\"tradingHalt\">정지</button>", Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -66,7 +77,9 @@ public sealed class LegacyWebContractTests
|
||||
Assert.Contains("timestampMilliseconds", App, StringComparison.Ordinal);
|
||||
Assert.Contains("event.ctrlKey", App, StringComparison.Ordinal);
|
||||
Assert.Contains("event.shiftKey", App, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("dblclick", App, StringComparison.Ordinal);
|
||||
var cutFactory = App[
|
||||
App.IndexOf("function createCutElement", StringComparison.Ordinal)..App.IndexOf("function renderCuts", StringComparison.Ordinal)];
|
||||
Assert.DoesNotContain("dblclick", cutFactory, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -86,6 +99,161 @@ public sealed class LegacyWebContractTests
|
||||
Assert.DoesNotContain("playlistRows.replaceChildren", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlaylistRowsSendSelectionAndMutationIntentsOnly()
|
||||
{
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"select-playlist-row",
|
||||
"set-playlist-enabled",
|
||||
"move-selected-playlist-rows",
|
||||
"delete-selected-playlist-rows",
|
||||
"delete-all-playlist-rows"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"send(\"{intent}\"", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("control: event.ctrlKey", App, StringComparison.Ordinal);
|
||||
Assert.Contains("shift: event.shiftKey", App, StringComparison.Ordinal);
|
||||
Assert.Contains("event.key === \"Delete\"", App, StringComparison.Ordinal);
|
||||
Assert.Contains("row.isSelected", App, StringComparison.Ordinal);
|
||||
Assert.Contains("enabled.disabled = state.isBusy === true", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlaylistReorderUsesKeyedDomNodesAndPreservesFocus()
|
||||
{
|
||||
Assert.Contains("const existingRows = new Map()", App, StringComparison.Ordinal);
|
||||
Assert.Contains("existingRows.get(row.rowId)", App, StringComparison.Ordinal);
|
||||
Assert.Contains("playlistRows.insertBefore(element, elementAtPosition)", App, StringComparison.Ordinal);
|
||||
Assert.Contains("const retainedRowIds = new Set", App, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("playlistRows.replaceChild", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FixedCatalogUsesOpaqueCSharpActionAndSectionIntents()
|
||||
{
|
||||
Assert.Contains("state.fixedCatalog", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.actionId", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.sectionIndex", App, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"activate-fixed-action\"", App, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"activate-fixed-section\"", App, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"activate-industry-section\"", App,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"activate-comparison-section\"", App,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.industrySectionIndex", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.comparisonSectionIndex", App, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"set-moving-averages\"", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndustryWorkspaceUsesIndexesAndOpaqueActionIds()
|
||||
{
|
||||
Assert.Contains("state.industry", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.industryIndex", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.industryActionId", App, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"select-industry\"", App, StringComparison.Ordinal);
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComparisonWorkspaceUsesOnlyOpaqueTargetPairAndActionIds()
|
||||
{
|
||||
foreach (var value in new[]
|
||||
{
|
||||
"search-comparison-domestic",
|
||||
"search-comparison-world",
|
||||
"select-comparison-domestic",
|
||||
"select-comparison-world",
|
||||
"select-comparison-fixed",
|
||||
"choose-comparison-domestic",
|
||||
"choose-comparison-world",
|
||||
"choose-comparison-fixed",
|
||||
"swap-comparison-targets",
|
||||
"clear-comparison-targets",
|
||||
"add-comparison-pair",
|
||||
"select-comparison-pair",
|
||||
"move-comparison-pair",
|
||||
"delete-selected-comparison-pair",
|
||||
"delete-all-comparison-pairs",
|
||||
"activate-comparison-action"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"\"{value}\"", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("dataset.comparisonSelectionId", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.comparisonPairId", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.comparisonActionId", App, StringComparison.Ordinal);
|
||||
Assert.Contains("comparisonTargetClickTimer", App, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("stockCode", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThemeWorkspaceUsesOpaqueSelectionSortAndActionIds()
|
||||
{
|
||||
Assert.Contains("state.theme", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.themeResultId", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.themeActionId", App, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"search-themes\"", App, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"select-theme\"", App, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"set-theme-sort\"", App, StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"activate-theme-action\"", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpertAndTradingHaltWorkspacesUseOpaqueIntents()
|
||||
{
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"search-experts",
|
||||
"select-expert",
|
||||
"activate-expert-action",
|
||||
"search-trading-halts",
|
||||
"select-trading-halt",
|
||||
"toggle-trading-halt-sort",
|
||||
"activate-trading-halt-action"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"send(\"{intent}\"", App, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DbSaveLoadUseClosedNamedPlaylistIntentsWhilePlayoutRemainsDisabled()
|
||||
{
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"refresh-named-playlists",
|
||||
"select-named-playlist",
|
||||
"load-selected-named-playlist",
|
||||
"create-named-playlist",
|
||||
"save-current-named-playlist",
|
||||
"delete-selected-named-playlist"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"\"{intent}\"", App, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("state.namedPlaylist", App, StringComparison.Ordinal);
|
||||
Assert.Contains("dataset.namedPlaylistDefinitionId", App, StringComparison.Ordinal);
|
||||
Assert.Contains("definitionId: definition.dataset.namedPlaylistDefinitionId", App,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("dbLoadButton.disabled = busy || !named", App, StringComparison.Ordinal);
|
||||
Assert.Contains("named.isWriteQuarantined", App, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("programCode", App, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("listText", App, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var label in new[] { "TAKE IN", "NEXT", "TAKE OUT" })
|
||||
{
|
||||
Assert.Matches($"<button[^>]*disabled[^>]*>[^<]*{Regex.Escape(label)}", Markup);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeparatorRowsPreserveOriginalDragGesture()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user