feat: complete legacy UI and playout parity migration

This commit is contained in:
2026-07-15 13:11:38 +09:00
parent f14800656b
commit 6e0c275fdd
108 changed files with 43738 additions and 560 deletions

View File

@@ -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.");
}
}

View File

@@ -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.");
}
}

View File

@@ -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.");
}
}

View File

@@ -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.");
}
}

View File

@@ -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.");
}
}

View File

@@ -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.");
}
}

View File

@@ -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.");
}
}

View File

@@ -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.");
}
}

View File

@@ -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.");
}
}

View File

@@ -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.");
}
}

View File

@@ -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.");
}
}

View File

@@ -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()
{