feat: complete legacy parity and modernize operator UI

This commit is contained in:
2026-07-22 12:34:46 +09:00
parent fc4007d676
commit 939c252d23
149 changed files with 26515 additions and 1736 deletions

View File

@@ -0,0 +1,60 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyComparisonImportWebTests
{
private static readonly string RepositoryRoot = FindRepositoryRoot();
private static readonly string App = Read("src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web", "app.js");
private static readonly string Styles = Read("src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web", "styles.css");
private static readonly string MainWindow = Read("src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "MainWindow.xaml.cs");
private static readonly string Bridge = Read("src", "MBN_STOCK_WEBVIEW.LegacyBridge", "LegacyBridgeProtocol.cs");
[Fact]
public void Comparison_workspace_exposes_explicit_fixed_source_merge_and_result()
{
Assert.Contains("importLegacy.textContent = \"원본 비교쌍 가져오기\"", App,
StringComparison.Ordinal);
Assert.Contains("importLegacy.dataset.comparisonImport = \"true\"", App,
StringComparison.Ordinal);
Assert.Contains("send(\"import-legacy-comparison-pairs\", {})", App,
StringComparison.Ordinal);
Assert.Contains("comparison.lastImport.sourceRowCount", App,
StringComparison.Ordinal);
Assert.Contains("comparison.lastImport.addedPairCount", App,
StringComparison.Ordinal);
Assert.Contains(".comparison-import-status", Styles, StringComparison.Ordinal);
Assert.DoesNotContain("comparisonImportPath", App, StringComparison.Ordinal);
}
[Fact]
public void Native_composition_owns_the_trusted_source_and_database_resolver()
{
Assert.Contains("TrustedLegacyComparisonFileSource.CreateDefault()", MainWindow,
StringComparison.Ordinal);
Assert.Contains("new LegacyComparisonPairImportService(", MainWindow,
StringComparison.Ordinal);
Assert.Contains("comparisonImporter)", MainWindow, StringComparison.Ordinal);
Assert.Contains("\"import-legacy-comparison-pairs\" => ParseComparisonImport(payload)",
Bridge, StringComparison.Ordinal);
Assert.Contains("HasOnlyProperties(payload) ? new LegacyImportComparisonPairsIntent()",
Bridge, StringComparison.Ordinal);
}
private static string Read(params string[] segments) =>
File.ReadAllText(Path.Combine([RepositoryRoot, .. segments]));
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,160 @@
using System.Text.RegularExpressions;
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyExplicitModalExitContractTests
{
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 ModalFocus = File.ReadAllText(
Path.Combine(WebRoot, "legacy-modal-focus.js"));
private static readonly string InputSmoke = File.ReadAllText(Path.Combine(
RepositoryRoot,
"scripts",
"Test-LegacyPackageInput.mjs"));
[Theory]
[InlineData(
"manual-financial-workspace",
"manual-financial-exit",
"나가기")]
[InlineData(
"manual-list-workspace",
"manual-list-cancel",
"취소")]
public void Original_text_exit_is_a_focusable_footer_button(
string workspaceId,
string buttonId,
string label)
{
Assert.Matches(
$"id=\"{workspaceId}\"[\\s\\S]*?<footer class=\"manual-dialog-footer\">" +
$"\\s*<button id=\"{buttonId}\" type=\"button\">{label}</button>",
Markup);
Assert.DoesNotMatch(
$"<button id=\"{buttonId}\"[^>]*(?:disabled|tabindex=\"-1\")",
Markup);
Assert.Contains("\"button\"", ModalFocus, StringComparison.Ordinal);
}
[Theory]
[InlineData(
"closeManualFinancial",
"manualClose",
"manualExit",
"close-manual-financial")]
[InlineData(
"closeManualList",
"manualListClose",
"manualListCancel",
"close-manual-list")]
public void Text_and_top_exit_share_one_close_handler(
string handler,
string topButton,
string textButton,
string intent)
{
Assert.Contains($"function {handler}()", App, StringComparison.Ordinal);
Assert.Contains(
$"{topButton}.addEventListener(\"click\", {handler})",
App,
StringComparison.Ordinal);
Assert.Contains(
$"{textButton}.addEventListener(\"click\", {handler})",
App,
StringComparison.Ordinal);
var handlerStart = App.IndexOf($"function {handler}()", StringComparison.Ordinal);
var nextListener = App.IndexOf(
$"{topButton}.addEventListener",
handlerStart,
StringComparison.Ordinal);
Assert.True(handlerStart >= 0 && nextListener > handlerStart);
var body = App[handlerStart..nextListener];
Assert.Single(Regex.Matches(body, $"send\\(\"{intent}\"").Cast<Match>());
}
[Fact]
public void Text_exit_lock_state_tracks_the_existing_top_exit()
{
Assert.Contains(
"manualExit.disabled = manualClose.disabled;",
App,
StringComparison.Ordinal);
Assert.Contains(
"manualListCancel.disabled = manualListClose.disabled;",
App,
StringComparison.Ordinal);
Assert.Contains(
"root.querySelectorAll(\"button, input, select, textarea\")",
App,
StringComparison.Ordinal);
Assert.Contains(
"setTransientControlsBusy(manualDialog, state.isBusy === true);",
App,
StringComparison.Ordinal);
Assert.Contains(
"setTransientControlsBusy(manualListDialog, busy);",
App,
StringComparison.Ordinal);
Assert.Matches(
@"\.manual-financial-dialog\s*\{[^}]*grid-template-rows:\s*46px minmax\(0, 1fr\) auto",
Styles);
Assert.Matches(
@"\.manual-list-dialog\s*\{[^}]*grid-template-rows:\s*46px minmax\(0, 1fr\) auto",
Styles);
}
[Fact]
public void Full_UI_input_validation_clicks_the_restored_text_exit_controls()
{
Assert.Matches(
@"async function closeManualFinancial[\s\S]*?pointerClick\('document\.getElementById\(""manual-financial-exit""\)'",
InputSmoke);
Assert.Matches(
@"async function closeManualList[\s\S]*?pointerClick\('document\.getElementById\(""manual-list-cancel""\)'",
InputSmoke);
}
[Theory]
[InlineData("overseasStocks", "해외시장")]
[InlineData("tradingHalt", "거래정지")]
public void Clear_sidebar_name_preserves_the_existing_tab_identity(
string tabId,
string displayName)
{
var match = Regex.Match(
Markup,
$"<button[^>]*data-tab-id=\"{tabId}\"[\\s\\S]*?</button>");
Assert.True(match.Success);
Assert.Contains($"aria-label=\"{displayName}\"", match.Value,
StringComparison.Ordinal);
Assert.Contains($"title=\"{displayName}\"", match.Value,
StringComparison.Ordinal);
Assert.Contains($">{displayName}</span>", match.Value,
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

@@ -34,6 +34,8 @@ public sealed class LegacyFixedSectionBatchWebTests
StringComparison.Ordinal);
Assert.Contains("section.getAttribute(\"aria-disabled\") !== \"true\"", App,
StringComparison.Ordinal);
Assert.Contains("send(\"activate-fixed-section\"", App,
StringComparison.Ordinal);
}
[Fact]
@@ -63,6 +65,20 @@ public sealed class LegacyFixedSectionBatchWebTests
Assert.Contains("cancellationToken", MainWindow, StringComparison.Ordinal);
}
[Fact]
public void Native_fixed_page_preflight_keeps_the_catalog_busy_until_database_read_finishes()
{
var busyIntents = Extract(
MainWindow,
"if ((intent is LegacySearchStocksIntent",
"await AcquirePriorityDatabaseActivityAsync");
Assert.Contains("LegacyActivateFixedActionIntent", busyIntents,
StringComparison.Ordinal);
Assert.Contains("LegacyActivateFixedSectionIntent", busyIntents,
StringComparison.Ordinal);
}
[Fact]
public void Native_boundary_allows_only_current_manual_edits_until_batch_completion()
{

View File

@@ -0,0 +1,128 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyFsellPackageInputContractTests
{
private static readonly string Harness = File.ReadAllText(Path.Combine(
FindRepositoryRoot(),
"scripts",
"Test-LegacyPackageInput.mjs"));
[Fact]
public void FsellCellEditUsesPackagedPointerTextAndFocusChangeBeforeNativeDraftCheck()
{
var helper = Slice(
Harness,
"async function replaceManualNetSellCell",
"async function confirmManualNetSellAndVerifyReadback");
AssertInOrder(
helper,
"await replaceText(expression, value, label);",
"await pressKey(\"Tab\"",
"snapshot.state.revision > before.state.revision",
"snapshot.state.manualLists?.netRowsAreFresh === false",
"snapshot.state.manualLists?.writesQuarantined !== true",
"JSON.stringify(netSellProjection(snapshot)) === JSON.stringify(expectedRows)");
Assert.DoesNotContain("Runtime.evaluate", helper, StringComparison.Ordinal);
Assert.DoesNotContain(".value =", helper, StringComparison.Ordinal);
}
[Fact]
public void FsellSaveIsIssuedOnceAndRequiresCorrelatedFreshReadBeforeContinuing()
{
var helper = Slice(
Harness,
"async function confirmManualNetSellAndVerifyReadback",
"function viProjection");
Assert.Equal(
1,
helper.Split(
"await pointerClick(modalButtonByTextExpression(\"확인\"), label);",
StringSplitOptions.None).Length - 1);
AssertInOrder(
helper,
"before.state.manualLists?.canSave !== true",
"await pointerClick(modalButtonByTextExpression(\"확인\"), label);",
"snapshot.state.manualLists?.isOpen === false",
"snapshot.state.manualLists?.netRowsAreFresh === true",
"snapshot.state.manualLists?.writesQuarantined !== true",
"JSON.stringify(netSellProjection(snapshot)) === JSON.stringify(expectedRows)",
"snapshot.state.playlist.length === before.state.playlist.length + 1");
}
[Fact]
public void FsellRoundTripPersistsUniqueTemporaryValueThenRestoresExactBaseline()
{
var sequence = Slice(
Harness,
"\"fixed-245\", \"net-sell\", \"individual\", \"FSell save reopen\"");
Assert.Contains("randomBytes(3).readUIntBE(0, 3)", sequence,
StringComparison.Ordinal);
Assert.Equal(
2,
sequence.Split("await replaceManualNetSellCell(",
StringSplitOptions.None).Length - 1);
Assert.Equal(
2,
sequence.Split("await confirmManualNetSellAndVerifyReadback(",
StringSplitOptions.None).Length - 1);
AssertInOrder(
sequence,
"temporaryRows[0].leftAmount = temporaryLeftAmount;",
"\"FSell row 1 left amount temporary input\"",
"\"FSell temporary value confirm save\"",
"fsellRoundTrip.mutationSaveVerified = true;",
"\"FSell temporary value fresh read\"",
"fsellRoundTrip.mutationFreshReadVerified = true;",
"individualBaseline[0].leftAmount, individualBaseline",
"\"FSell original value confirm restore\"",
"fsellRoundTrip.restoreSaveVerified = true;",
"\"FSell restored fresh read\"",
"JSON.stringify(netSellProjection(current)) !== JSON.stringify(individualBaseline)",
"fsellRoundTrip.restoreFreshReadVerified = true;");
Assert.Contains("restored: true", sequence, StringComparison.Ordinal);
}
private static string Slice(string source, string start, string? end = null)
{
var startIndex = source.IndexOf(start, StringComparison.Ordinal);
Assert.True(startIndex >= 0, $"Start marker was not found: {start}");
if (end is null)
{
return source[startIndex..];
}
var endIndex = source.IndexOf(end, startIndex, StringComparison.Ordinal);
Assert.True(endIndex > startIndex, $"End marker was not found: {end}");
return source[startIndex..endIndex];
}
private static void AssertInOrder(string source, params string[] markers)
{
var cursor = 0;
foreach (var marker in markers)
{
var index = source.IndexOf(marker, cursor, StringComparison.Ordinal);
Assert.True(index >= cursor, $"Marker was missing or out of order: {marker}");
cursor = index + marker.Length;
}
}
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

@@ -4,12 +4,17 @@ namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyGraphClickParityWebTests
{
private static readonly string RepositoryRoot = FindRepositoryRoot();
private static readonly string App = File.ReadAllText(Path.Combine(
FindRepositoryRoot(),
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"Web",
"app.js"));
private static readonly string PackageInputHarness = File.ReadAllText(Path.Combine(
RepositoryRoot,
"scripts",
"Test-LegacyPackageInput.mjs"));
[Fact]
public void Uc3_targets_select_on_single_click_and_fill_slots_only_on_double_click()
@@ -52,7 +57,12 @@ public sealed class LegacyGraphClickParityWebTests
}
Assert.Contains("comparisonTargetClickTimer", singleClick, StringComparison.Ordinal);
Assert.Contains("}, 250);", singleClick, StringComparison.Ordinal);
Assert.Contains("beginComparisonTargetPendingSelection", singleClick,
StringComparison.Ordinal);
Assert.Contains("deferLegacySingleClick(function () {", singleClick,
StringComparison.Ordinal);
Assert.Contains("clearComparisonTargetPendingSelection(true)", singleClick,
StringComparison.Ordinal);
}
[Fact]
@@ -65,7 +75,7 @@ public sealed class LegacyGraphClickParityWebTests
Assert.Matches(
new Regex(
"addEventListener\\(\"click\"[\\s\\S]*?setTimeout[\\s\\S]*?send\\(\"select-manual-vi-result\"",
"addEventListener\\(\"click\"[\\s\\S]*?beginManualViResultPendingSelection[\\s\\S]*?deferLegacySingleClick[\\s\\S]*?send\\(\"select-manual-vi-result\"",
RegexOptions.CultureInvariant),
manual);
Assert.Matches(
@@ -75,6 +85,79 @@ public sealed class LegacyGraphClickParityWebTests
manual);
}
[Fact]
public void GraphE_stock_candidate_single_click_focuses_and_double_click_selects_once()
{
var doubleStart = App.IndexOf(
"manualWorkspace.addEventListener(\"dblclick\"",
StringComparison.Ordinal);
var clickStart = App.IndexOf(
"manualWorkspace.addEventListener(\"click\"",
doubleStart + 1,
StringComparison.Ordinal);
var keyStart = App.IndexOf(
"manualWorkspace.addEventListener(\"keydown\"",
clickStart + 1,
StringComparison.Ordinal);
Assert.True(doubleStart >= 0 && clickStart > doubleStart && keyStart > clickStart);
var doubleClick = App[doubleStart..clickStart];
var singleClick = App[clickStart..keyStart];
Assert.Contains("button[data-manual-stock-result-id]", doubleClick,
StringComparison.Ordinal);
Assert.Contains("selectManualFinancialStock(stock)", doubleClick,
StringComparison.Ordinal);
Assert.DoesNotContain("focusManualFinancialStock(stock)", doubleClick,
StringComparison.Ordinal);
Assert.Contains("focusManualFinancialStock(stock)", singleClick,
StringComparison.Ordinal);
Assert.DoesNotContain("queueManualFinancialStockSelection", App,
StringComparison.Ordinal);
var helperStart = App.IndexOf(
"function focusManualFinancialStock",
StringComparison.Ordinal);
var helperEnd = App.IndexOf(
"function captureManualDraftForChangedModel",
helperStart,
StringComparison.Ordinal);
Assert.True(helperStart >= 0 && helperEnd > helperStart);
var helper = App[helperStart..helperEnd];
Assert.DoesNotContain("deferLegacySingleClick", helper, StringComparison.Ordinal);
Assert.Contains("stock.focus({ preventScroll: true })", helper,
StringComparison.Ordinal);
Assert.Single(
Regex.Matches(helper, "send\\(\"select-manual-financial-stock\"")
.Cast<Match>());
var keyDown = App[keyStart..];
Assert.Contains("selectManualFinancialStock(stock)", keyDown,
StringComparison.Ordinal);
Assert.Contains("event.preventDefault();", keyDown, StringComparison.Ordinal);
}
[Fact]
public void Packaged_input_harness_double_clicks_GraphE_candidate_and_proves_one_intent()
{
Assert.Contains("beforeCandidateMessageSequence", PackageInputHarness,
StringComparison.Ordinal);
Assert.Contains("await pointerDoubleClick(", PackageInputHarness,
StringComparison.Ordinal);
Assert.Contains("select verified stock by slow Windows double-click", PackageInputHarness,
StringComparison.Ordinal);
Assert.Contains("message.sequence > beforeCandidateMessageSequence", PackageInputHarness,
StringComparison.Ordinal);
Assert.Contains("candidateMessages.length !== 1", PackageInputHarness,
StringComparison.Ordinal);
Assert.Contains("candidateMessages[0].type !== \"select-manual-financial-stock\"",
PackageInputHarness,
StringComparison.Ordinal);
Assert.Contains("await sleep(350);", PackageInputHarness, StringComparison.Ordinal);
Assert.Contains("SHOULD_CLEAR_ON_STOCK_CHANGE", PackageInputHarness,
StringComparison.Ordinal);
Assert.Contains("focusedCandidateId", PackageInputHarness, StringComparison.Ordinal);
}
private static string FindRepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);

View File

@@ -17,7 +17,7 @@ public sealed class LegacyHiddenNonOriginalControlsTests
"styles.css"));
[Fact]
public void Non_original_shortcuts_and_original_hidden_dissolve_panel_are_not_operator_controls()
public void NonOriginalShortcutsAndRuntimeClippedPrepareAndDissolveStayHidden()
{
Assert.Contains("class=\"manual-list-actions\" aria-label=\"수동 송출 목록\" hidden",
Markup,

View File

@@ -34,6 +34,15 @@ public sealed class LegacyInteractionMetricsNativeContractTests
Assert.DoesNotContain("GetSystemMetrics(", Window, StringComparison.Ordinal);
Assert.Contains("LegacyInteractionMetrics.FromPixelsAtDpi(", Window,
StringComparison.Ordinal);
Assert.Contains("private static extern uint GetDoubleClickTime();", Window,
StringComparison.Ordinal);
Assert.Contains(
"doubleClickTimeMilliseconds: (int)GetDoubleClickTime()",
Window,
StringComparison.Ordinal);
Assert.Equal(2, CountOccurrences(
Window,
"[DllImport(\"user32.dll\", ExactSpelling = true)]"));
Assert.Contains("logical DIP coordinate space used by XAML", Window,
StringComparison.Ordinal);
Assert.Contains("Root.XamlRoot?.RasterizationScale", Window,

View File

@@ -101,6 +101,73 @@ public sealed class LegacyManualFinancialParityWebTests
StringComparison.Ordinal);
}
[Fact]
public void GraphE_uses_only_the_four_same_origin_packaged_reference_images()
{
foreach (var relativePath in new[]
{
"./Previews/pie.bmp",
"./Previews/Grow.bmp",
"./Previews/sell.bmp",
"./Previews/profit.bmp"
})
{
Assert.Contains($"\"{relativePath}\"", App, StringComparison.Ordinal);
}
Assert.Contains("manualFinancialReferenceFigure", App, StringComparison.Ordinal);
Assert.Contains("기존 송출 화면 참고 이미지", App, StringComparison.Ordinal);
Assert.Contains(".manual-financial-reference img", Styles, StringComparison.Ordinal);
Assert.DoesNotContain("../Res/", App, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("legacy-res.mbn.local", App, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void GraphE_list_projects_bounded_read_only_preview_cells_without_native_authority()
{
var start = App.IndexOf("function manualFinancialPreviewColumns", StringComparison.Ordinal);
var end = App.IndexOf("function blankManualRecord", start, StringComparison.Ordinal);
Assert.True(start >= 0 && end > start);
var preview = App[start..end];
Assert.Contains("row.previewValues", preview, StringComparison.Ordinal);
Assert.Contains("slice(0, count)", preview, StringComparison.Ordinal);
Assert.Contains(
"const cell = manualElement(\"span\", \"manual-financial-preview-cell\", value);",
App,
StringComparison.Ordinal);
Assert.DoesNotMatch(
new Regex("(?:ROWID|ROW_HANDLE|rowVersion|stockCode|resultId)"),
preview);
Assert.Contains("manual-financial-preview-columns-", App, StringComparison.Ordinal);
Assert.Contains("manual-financial-preview-cell", App, StringComparison.Ordinal);
Assert.Contains(".manual-financial-preview-cell", Styles, StringComparison.Ordinal);
}
[Fact]
public void Explicit_different_stock_selection_resets_only_a_create_mode_draft()
{
Assert.Contains("function shouldResetManualStockDraft", App, StringComparison.Ordinal);
Assert.Contains("stockIsSelected !== true && stockNameReadOnly !== true", App,
StringComparison.Ordinal);
Assert.Contains("draft.rawMode !== true", App, StringComparison.Ordinal);
var start = App.IndexOf(
"function selectManualFinancialStock",
StringComparison.Ordinal);
var end = App.IndexOf(
"function captureManualDraftForChangedModel",
start,
StringComparison.Ordinal);
Assert.True(start >= 0 && end > start);
var helper = App[start..end];
Assert.Contains("rememberManualDraft();", helper, StringComparison.Ordinal);
Assert.Contains("manualDraft = blankManualRecord(manualDraft.screen);", helper,
StringComparison.Ordinal);
Assert.Contains("manualDraftResetPending = true;", helper, StringComparison.Ordinal);
Assert.Contains("manualDraft = previousDraft;", helper, StringComparison.Ordinal);
}
[Fact]
public void Manual_Web_section_sends_values_and_opaque_ids_without_DB_or_scene_authority()
{

View File

@@ -0,0 +1,141 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyNativeDialogParityWebTests
{
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 Controller = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyApplication",
"LegacyOperatorController.cs"));
private static readonly string PackageInputHarness = File.ReadAllText(Path.Combine(
RepositoryRoot,
"scripts",
"Test-LegacyPackageInput.mjs"));
[Fact]
public void Original_destructive_questions_are_authoritative_MmoneyCoder_dialogs()
{
foreach (var intent in new[]
{
"delete-all-playlist-rows",
"delete-manual-financial",
"delete-all-manual-financial",
"delete-all-manual-vi-items"
})
{
Assert.Contains($"send(\"{intent}\", {{}})", App, StringComparison.Ordinal);
}
Assert.DoesNotContain(
"window.confirm(\"송출 리스트를 모두 삭제하겠습니까?\")",
App,
StringComparison.Ordinal);
Assert.DoesNotContain(
"window.confirm(\"선택한 수동 재무 데이터를 삭제하시겠습니까?\")",
App,
StringComparison.Ordinal);
Assert.DoesNotContain(
"window.confirm(\"현재 화면의 수동 재무 데이터를 모두 삭제하시겠습니까?\")",
App,
StringComparison.Ordinal);
Assert.DoesNotContain(
"window.confirm(\"VI 목록을 모두 지우시겠습니까?\")",
App,
StringComparison.Ordinal);
Assert.DoesNotContain(
"clear.disabled = manual.viItems.length === 0",
App,
StringComparison.Ordinal);
foreach (var message in new[]
{
"송출 리스트를 모두 삭제하겠습니까?",
"선택한 종목을 삭제하겠습니까?",
"모든 종목을 삭제하겠습니까?",
"모두 삭제하겠습니까?"
})
{
Assert.Contains(message, Controller, StringComparison.Ordinal);
}
Assert.Contains("IsConfirmation: true", Controller, StringComparison.Ordinal);
Assert.Contains("PendingNativeConfirmation.ForPlaylistDeleteAll", Controller,
StringComparison.Ordinal);
Assert.Contains("PendingNativeConfirmation.ForManualFinancialDelete", Controller,
StringComparison.Ordinal);
Assert.Contains("PendingNativeConfirmation.ForManualViDeleteAll", Controller,
StringComparison.Ordinal);
}
[Fact]
public void PList_confirm_save_requests_state_driven_saved_dialog_without_browser_alert()
{
Assert.DoesNotContain(
"window.alert(\"저장되었습니다\")",
App,
StringComparison.Ordinal);
Assert.Contains(
"if (command === \"save-current-named-playlist-to\")",
App,
StringComparison.Ordinal);
Assert.Contains("payload.showSavedAlert = showSavedAlert === true", App,
StringComparison.Ordinal);
Assert.Contains("if (succeeded && showSavedAlert)", Controller,
StringComparison.Ordinal);
Assert.Contains("new LegacyDialogState(\"저장되었습니다\")", Controller,
StringComparison.Ordinal);
}
[Fact]
public void Package_input_smoke_answers_the_native_dialogs_it_now_exercises()
{
foreach (var label in new[]
{
"PList confirm row save",
"GraphE ${screen} save",
"GraphE ${screen} delete",
"GraphE ${screen} delete-all cancel branch",
"VIList delete-all cancel branch",
"screen validation playlist cleanup"
})
{
Assert.Contains(label, PackageInputHarness, StringComparison.Ordinal);
}
Assert.Equal(
7,
PackageInputHarness.Split(
"pointerClickWithNativeDialog(",
StringSplitOptions.None).Length - 1);
// The only remaining browser confirm is the separate one-time import
// prompt, which has not been migrated to LegacyDialogState.
Assert.Equal(
2,
PackageInputHarness.Split(
"pointerClickWithJavaScriptDialog(",
StringSplitOptions.None).Length - 1);
}
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

@@ -70,6 +70,8 @@ public sealed class LegacyOperatorCatalogParityWebTests
"operator-catalog-query",
"operator-catalog-new",
"operator-catalog-results",
"operator-catalog-editor-market-field",
"operator-catalog-editor-market",
"operator-catalog-name",
"operator-catalog-name-check",
"operator-catalog-name-status",
@@ -91,6 +93,13 @@ public sealed class LegacyOperatorCatalogParityWebTests
Assert.Contains("operator-catalog-backdrop[hidden]", Styles, StringComparison.Ordinal);
Assert.Contains("operatorCatalogStockResults.addEventListener(\"dblclick\"", App,
StringComparison.Ordinal);
Assert.Contains("function reconcileOperatorCatalogStockResults(rows)", App,
StringComparison.Ordinal);
Assert.Contains("operatorCatalogStockResults.insertBefore(button, buttonAtPosition)", App,
StringComparison.Ordinal);
Assert.DoesNotContain("operatorCatalogStockClickTimer", App, StringComparison.Ordinal);
Assert.DoesNotContain("window.setTimeout(function () {\n" +
" operatorCatalogStockClickTimer", App, StringComparison.Ordinal);
Assert.Contains("dataset.operatorCatalogBuyAmountRowId = row.rowId", App,
StringComparison.Ordinal);
Assert.Contains("dataset.operatorCatalogRowId = row.rowId", App,
@@ -117,6 +126,98 @@ public sealed class LegacyOperatorCatalogParityWebTests
StringComparison.Ordinal);
}
[Fact]
public void ThemeA_create_editor_can_change_market_without_discarding_the_typed_name()
{
Assert.Contains(
"const canChangeCreateThemeMarket = isTheme && catalog.mode === \"create\";",
App,
StringComparison.Ordinal);
Assert.Contains(
"operatorCatalogEditorMarketField.hidden = !canChangeCreateThemeMarket;",
App,
StringComparison.Ordinal);
Assert.Contains(
"operatorCatalogEditorMarket.disabled = !canChangeCreateThemeMarket;",
App,
StringComparison.Ordinal);
Assert.Contains(
"root.querySelectorAll(\"button, input, select, textarea\")",
App,
StringComparison.Ordinal);
Assert.Contains(
"operatorCatalogEditorMarket.value = catalog.newThemeMarket === \"nxt\" ? \"nxt\" : \"krx\";",
App,
StringComparison.Ordinal);
var start = App.IndexOf(
"operatorCatalogEditorMarket.addEventListener(\"change\"",
StringComparison.Ordinal);
var end = App.IndexOf(
"operatorCatalogResults.addEventListener",
start,
StringComparison.Ordinal);
Assert.True(start >= 0 && end > start);
var marketChange = App[start..end];
Assert.Contains("operatorCatalogState.mode !== \"create\"", marketChange,
StringComparison.Ordinal);
Assert.Contains("operatorCatalogNameDraft = operatorCatalogName.value;", marketChange,
StringComparison.Ordinal);
Assert.Contains("send(\"change-create-theme-market\", {", marketChange,
StringComparison.Ordinal);
Assert.Contains("editorName: operatorCatalogName.value.trim()", marketChange,
StringComparison.Ordinal);
Assert.DoesNotContain("operatorCatalogDraftKey = \"\"", marketChange,
StringComparison.Ordinal);
Assert.DoesNotContain("operatorCatalogNameDraft = \"\"", marketChange,
StringComparison.Ordinal);
Assert.DoesNotContain("begin-create-theme-catalog", marketChange,
StringComparison.Ordinal);
var newButtonStart = App.IndexOf(
"operatorCatalogNew.addEventListener(\"click\"",
StringComparison.Ordinal);
Assert.True(newButtonStart >= 0 && start > newButtonStart);
var listCreate = App[newButtonStart..start];
Assert.Contains("send(\"begin-create-theme-catalog\", {", listCreate,
StringComparison.Ordinal);
Assert.Contains("market: operatorCatalogNewMarket.value", listCreate,
StringComparison.Ordinal);
}
[Fact]
public void ThemeA_and_UC6_stock_picker_send_blank_button_and_Enter_searches_without_breaking_IME()
{
var start = App.IndexOf(
"function searchOperatorCatalogStocks()",
StringComparison.Ordinal);
var end = App.IndexOf(
"operatorCatalogStockResults.addEventListener",
start,
StringComparison.Ordinal);
Assert.True(start >= 0 && end > start);
var stockPicker = App[start..end];
Assert.Contains(
"const query = operatorCatalogStockQuery.value.trim();",
stockPicker,
StringComparison.Ordinal);
Assert.Contains(
"send(\"search-operator-catalog-stocks\", { query: query });",
stockPicker,
StringComparison.Ordinal);
Assert.DoesNotContain("if (query)", stockPicker, StringComparison.Ordinal);
Assert.Contains(
"operatorCatalogStockSearch.addEventListener(\"click\", searchOperatorCatalogStocks);",
stockPicker,
StringComparison.Ordinal);
Assert.Contains(
"event.key === \"Enter\" && !isImeComposing(event)",
stockPicker,
StringComparison.Ordinal);
Assert.Contains("searchOperatorCatalogStocks();", stockPicker, StringComparison.Ordinal);
}
[Fact]
public void Web_sends_closed_intents_and_never_owns_catalog_DB_identity()
{
@@ -161,6 +262,14 @@ public sealed class LegacyOperatorCatalogParityWebTests
StringComparison.Ordinal);
Assert.Contains("send(\"close-operator-catalog\", {})", App,
StringComparison.Ordinal);
Assert.Contains(
"const preserveStoredThemeName = operatorCatalogState &&",
App,
StringComparison.Ordinal);
Assert.Contains(
"const name = preserveStoredThemeName ? rawName : canonicalName;",
App,
StringComparison.Ordinal);
Assert.Contains("rowId: remove.dataset.operatorCatalogRemoveRowId", App,
StringComparison.Ordinal);
Assert.DoesNotContain(
@@ -237,6 +346,10 @@ public sealed class LegacyOperatorCatalogParityWebTests
"new LegacyOperatorCatalogSchemaValidationService(",
MainWindow,
StringComparison.Ordinal);
Assert.Contains(
"new LegacyOperatorCatalogStockSearchService(sharedStockSearchService)",
MainWindow,
StringComparison.Ordinal);
Assert.Contains("LegacyOpenOperatorCatalogIntent", MainWindow, StringComparison.Ordinal);
Assert.Contains("LegacyBeginUc4ThemeCatalogIntent", MainWindow, StringComparison.Ordinal);
Assert.Contains("LegacyEditUc4SelectedThemeIntent", MainWindow, StringComparison.Ordinal);
@@ -244,11 +357,24 @@ public sealed class LegacyOperatorCatalogParityWebTests
Assert.Contains("LegacyBeginUc6ExpertCatalogIntent", MainWindow, StringComparison.Ordinal);
Assert.Contains("LegacyEditUc6SelectedExpertIntent", MainWindow, StringComparison.Ordinal);
Assert.Contains("LegacyDeleteUc6SelectedExpertIntent", MainWindow, StringComparison.Ordinal);
Assert.Contains("LegacyChangeCreateThemeMarketIntent", MainWindow,
StringComparison.Ordinal);
Assert.Contains("LegacyCheckOperatorCatalogThemeNameIntent", MainWindow,
StringComparison.Ordinal);
Assert.Contains("LegacySaveOperatorCatalogIntent", MainWindow, StringComparison.Ordinal);
Assert.Contains("LegacySelectOperatorCatalogDraftRowIntent", MainWindow,
StringComparison.Ordinal);
var queuedSelectionStart = MainWindow.IndexOf(
"private static bool IsQueuedLocalSelectionIntent", StringComparison.Ordinal);
var queuedSelectionEnd = MainWindow.IndexOf(
"private static bool IsOperatorSettingsIntent", queuedSelectionStart,
StringComparison.Ordinal);
Assert.True(queuedSelectionStart >= 0 && queuedSelectionEnd > queuedSelectionStart);
var queuedSelections = MainWindow[queuedSelectionStart..queuedSelectionEnd];
Assert.Contains("LegacySelectOperatorCatalogStockIntent", queuedSelections,
StringComparison.Ordinal);
Assert.Contains("LegacyActivateOperatorCatalogStockIntent", queuedSelections,
StringComparison.Ordinal);
Assert.Contains("LegacyRemoveActiveOperatorCatalogDraftRowIntent", MainWindow,
StringComparison.Ordinal);
Assert.Contains("LegacyConfirmNativeDialogIntent", MainWindow,
@@ -266,6 +392,7 @@ public sealed class LegacyOperatorCatalogParityWebTests
foreach (var intent in new[]
{
"LegacyBeginCreateThemeCatalogIntent",
"LegacyChangeCreateThemeMarketIntent",
"LegacyBeginCreateExpertCatalogIntent",
"LegacyBeginUc4ThemeCatalogIntent",
"LegacyEditUc4SelectedThemeIntent",

View File

@@ -27,7 +27,9 @@ public sealed class LegacyOverseasParityWebTests
Assert.Contains("periodButtons(\"fixedIndex\")", render, StringComparison.Ordinal);
Assert.Contains("periodButtons(kind)", render, StringComparison.Ordinal);
Assert.Contains("model.isTruncated === true", render, StringComparison.Ordinal);
Assert.Contains("검색 결과가 최대 500건으로 제한되었습니다.", render, StringComparison.Ordinal);
Assert.Contains("검색 결과가 안전 상한을 초과해 일부만 표시됩니다.", render, StringComparison.Ordinal);
Assert.Contains("표시명 없음 ", render, StringComparison.Ordinal);
Assert.DoesNotContain("최대 500건", render, StringComparison.Ordinal);
Assert.Contains("overseas-truncation", Styles, StringComparison.Ordinal);
Assert.DoesNotContain("overseas-action-panel", render, StringComparison.Ordinal);
Assert.DoesNotContain("overseas-selected", render, StringComparison.Ordinal);

View File

@@ -0,0 +1,564 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyPackageInputSmokeContractTests
{
private static readonly string Runner = File.ReadAllText(Path.Combine(
FindRepositoryRoot(),
"scripts",
"Invoke-LegacyPackageInputSmoke.ps1"));
private static readonly string Harness = File.ReadAllText(Path.Combine(
FindRepositoryRoot(),
"scripts",
"Test-LegacyPackageInput.mjs"));
[Fact]
public void WindowsInputEvidenceIsMaterializedBeforePowerShellArrayWrapping()
{
Assert.Contains(
"WindowsInput = @($windowsInputEvidence.ToArray())",
Runner,
StringComparison.Ordinal);
Assert.DoesNotContain(
"WindowsInput = @($windowsInputEvidence)",
Runner,
StringComparison.Ordinal);
}
[Fact]
public void DryRunFinalSealKeepsWindowsDragOrderAndRequiresTheSafeTailAppend()
{
var seal = Slice(
Runner,
"$expectedAfterOrder = @(",
"$expectedFullUiDbPlaylistCount =");
Assert.Contains("$expectedFinalOrder = @($expectedAfterOrder)", seal,
StringComparison.Ordinal);
Assert.Contains("if ($HarnessProfile -ceq 'DryRunPlayout')", seal,
StringComparison.Ordinal);
Assert.Contains("[string]$evidence.dryRunPlayout.tailAppendedEntryId", seal,
StringComparison.Ordinal);
Assert.Equal(
2,
seal.Split(
"Test-ExactStringSequence $statePlaylistIds $expectedFinalOrder",
StringSplitOptions.None).Length - 1 +
seal.Split(
"Test-ExactStringSequence $domPlaylistIds $expectedFinalOrder",
StringSplitOptions.None).Length - 1);
Assert.DoesNotContain(
"Test-ExactStringSequence $statePlaylistIds $expectedAfterOrder",
seal,
StringComparison.Ordinal);
Assert.DoesNotContain(
"Test-ExactStringSequence $domPlaylistIds $expectedAfterOrder",
seal,
StringComparison.Ordinal);
}
[Fact]
public void DryRunSealPinsProgramEnableAndShortcutEvidenceExactly()
{
var seal = Slice(
Runner,
"$playoutIntentContractOk =",
"if ([int]$evidence.schemaVersion");
foreach (var token in new[]
{
"programEnableParity.currentEntryId",
"programEnableParity.skippedEntryId",
"programEnableParity.nextEntryId",
"programEnableParity.setAllIntentCount -eq 2",
"programEnableParity.toggleActiveIntentCount -eq 1",
"programEnableParity.endOfPlaylistObserved -eq $true",
"shortcutParity.f8IntentCount -eq 1",
"shortcutParity.escapeIntentCount -eq 1",
"shortcutParity.finalPhase -ceq 'idle'",
"[string]$_ -ceq 'set-all-playlist-enabled'",
"[string]$_ -ceq 'toggle-active-playlist-enabled'"
})
{
Assert.Contains(token, seal, StringComparison.Ordinal);
}
var allowlist = Slice(
Runner,
"$allowedOutbound = @(",
"foreach ($messageType");
Assert.Contains("'set-all-playlist-enabled'", allowlist,
StringComparison.Ordinal);
Assert.Contains("'toggle-active-playlist-enabled'", allowlist,
StringComparison.Ordinal);
}
[Fact]
public void NamedPlaylistRecoveryIsStrictlyScopedForwardedAndSealed()
{
Assert.Contains("[string]$RecoverNamedPlaylistTitle", Runner,
StringComparison.Ordinal);
Assert.Contains("'^CDX_P_[0-9]{13}_[0-9a-f]{6}$'", Runner,
StringComparison.Ordinal);
Assert.Contains("$RecoverNamedPlaylistTitle.Length -ne 26", Runner,
StringComparison.Ordinal);
Assert.Contains("$Profile -cne 'FullUiDb'", Runner,
StringComparison.Ordinal);
Assert.Contains("$FullUiDbScope -cne 'PList'", Runner,
StringComparison.Ordinal);
Assert.Contains("$FullUiDbScope -cne 'All'", Runner,
StringComparison.Ordinal);
Assert.Contains("'--recover-named-playlist-title'", Runner,
StringComparison.Ordinal);
var seal = Slice(
Runner,
"$expectedRecoveryRequested =",
"$databaseWriteContractOk =");
AssertInOrder(
seal,
"$recoveryEvidence.targetTitle -cne $ExpectedRecoveryTitle",
"$recoveryEvidence.freshListVerified -ne $true",
"$recoveryEvidence.exactAbsenceVerified -ne $true",
"$sealedRecoveryMatchCount -ne 0",
"$recoveryEvidence.result -ceq 'alreadyAbsent'",
"$recoveryEvidence.writeCount -eq 0",
"$namedPlaylistDeleteWriteCount -eq 1",
"$recoveryEvidence.result -ceq 'deleted'",
"$recoveryEvidence.writeCount -eq 1",
"@('committedFresh', 'committedOptimistic')",
"$namedPlaylistDeleteWriteCount -eq 2");
Assert.Contains("-not $namedPlaylistRecoveryContractOk", Runner,
StringComparison.Ordinal);
}
[Fact]
public void NamedPlaylistRecoveryUsesOneConfirmedDeleteWithoutRetryThenRunsCrud()
{
Assert.Contains("\"recover-named-playlist-title\"", Harness,
StringComparison.Ordinal);
Assert.Contains("/^CDX_P_[0-9]{13}_[0-9a-f]{6}$/", Harness,
StringComparison.Ordinal);
Assert.Contains("recoverNamedPlaylistTitle.length !== 26", Harness,
StringComparison.Ordinal);
Assert.Contains("new Set([\"all\", \"plist\"])", Harness,
StringComparison.Ordinal);
Assert.Contains(
"completedNamedPlaylistRefreshAfter(snapshot, recoveryRefreshBaseline)",
Harness,
StringComparison.Ordinal);
var recovery = Slice(
Harness,
"async function recoverNamedPlaylistFixture",
"async function executeNamedPlaylistCrud");
AssertInOrder(
recovery,
"if (initialWriteCount !== 0)",
"PList recovery fresh list open",
"completedNamedPlaylistRefreshAfter(snapshot, recoveryRefreshBaseline)",
"current.state.namedPlaylist.isListTruncated !== false",
"recovery.freshListVerified = true",
"const exactMatches =",
"if (exactMatches.length > 1)",
"if (exactMatches.length === 0)",
"recovery.result = \"alreadyAbsent\"",
"PList recovery exact target select",
"snapshot.state.namedPlaylist.selectedDefinitionId === target.definitionId",
"PList recovery open existing delete confirmation",
"snapshot.dom.namedConfirmationHidden === false",
"PList recovery confirm exact delete once",
"writeCount === 1",
"outcome === \"committedFresh\" || outcome === \"committedOptimistic\"",
"recovery.result = \"deleted\"",
"recovery.writeCount = recoveryWriteCount",
"recovery.exactAbsenceVerified = true");
Assert.Equal(
1,
recovery.Split(
"document.getElementById(\"named-playlist-confirmation-yes\")",
StringSplitOptions.None).Length - 1);
Assert.Contains("failUnknown(`Timed out waiting for ${label}; no action was retried.",
Harness,
StringComparison.Ordinal);
Assert.Contains("named.isWriteQuarantined === true", Harness,
StringComparison.Ordinal);
Assert.Contains("named.lastMutationOutcome === \"outcomeUnknown\"", Harness,
StringComparison.Ordinal);
var sequence = Slice(
Harness,
"async function executeFullUiDbSequence",
"try {");
AssertInOrder(
sequence,
"await recoverNamedPlaylistFixture();",
"await executeNamedPlaylistCrud();");
}
[Fact]
public void FullUiDbSafetyGateAllowsAndClassifiesEveryManualFinancialWriteIntent()
{
var allowed = Slice(
Harness,
"const fullUiDbAllowedOutboundMessageTypes",
"const playoutIntentMessageTypes");
var databaseWrites = Slice(
Harness,
"const databaseWriteIntentMessageTypes",
"const forbiddenCdpMethods");
foreach (var intent in new[]
{
"save-manual-financial",
"save-manual-financial-raw",
"delete-manual-financial",
"delete-all-manual-financial"
})
{
Assert.Contains($"\"{intent}\"", allowed, StringComparison.Ordinal);
Assert.Contains($"\"{intent}\"", databaseWrites, StringComparison.Ordinal);
}
}
[Fact]
public void FullUiDbCatalogChecksUseTheOriginalDirectInputGestures()
{
Assert.Contains("async function addOperatorCatalogStockByDoubleClick", Harness,
StringComparison.Ordinal);
Assert.Contains("interClickDelayMilliseconds", Harness, StringComparison.Ordinal);
Assert.Contains("market === \"krx\" ? 350 : 50", Harness,
StringComparison.Ordinal);
Assert.Contains("await pressKey(\"Delete\"", Harness, StringComparison.Ordinal);
Assert.Contains("async function dragOperatorCatalogRow", Harness,
StringComparison.Ordinal);
Assert.Contains("text/x-mbn-operator-catalog-row", Harness,
StringComparison.Ordinal);
var theme = Slice(
Harness,
"async function createReadEditDeleteTheme",
"async function searchAndSelectMainExpert");
AssertInOrder(
theme,
"addOperatorCatalogStockByDoubleClick(",
"third row Up",
"third row Down",
"focus third row for Delete key",
"pressKey(\"Delete\"");
var expert = Slice(
Harness,
"async function createReadEditDeleteExpert",
"async function executeOperatorCatalogCrud");
AssertInOrder(
expert,
"addOperatorCatalogStockByDoubleClick(",
"second row Up",
"second row Down",
"dragOperatorCatalogRow(",
"focus second row for Delete key",
"pressKey(\"Delete\"");
var fixedCatalog = Slice(
Harness,
"async function verifyFixedAndIndustryScreens",
"async function verifyComparisonScreen");
Assert.Contains("UC1 fixed parent batch", fixedCatalog,
StringComparison.Ordinal);
Assert.Contains("fixedSection.actions.length", fixedCatalog,
StringComparison.Ordinal);
Assert.Contains("beforeSection.safety.outboundMessages.length + 1", fixedCatalog,
StringComparison.Ordinal);
Assert.DoesNotContain("original no-op", fixedCatalog,
StringComparison.Ordinal);
}
[Fact]
public void FullUiDbKrxThemeCreatePhysicallyRoundTripsTheMarketWithoutLosingTheName()
{
var allowed = Slice(
Harness,
"const fullUiDbAllowedOutboundMessageTypes",
"const playoutIntentMessageTypes");
Assert.Contains("\"change-create-theme-market\"", allowed,
StringComparison.Ordinal);
var roundTrip = Slice(
Harness,
"async function verifyKrxThemeCreateMarketRoundTrip",
"async function createReadEditDeleteTheme");
AssertInOrder(
roundTrip,
"operatorCatalogEditorMarketValue !== \"krx\"",
"draftRows?.some(row => row.rowId === draftRowId)",
"switch editor market to NXT",
"newThemeMarket === \"nxt\"",
"editorName === title",
"draftRows?.length === 0",
"statusKind === \"warning\"",
"operatorCatalogNameValue === title",
"change-create-theme-market",
"switch editor market back to KRX",
"newThemeMarket === \"krx\"",
"operatorCatalogNameValue === title",
"removedDraftRowId: draftRowId");
var create = Slice(
Harness,
"async function createReadEditDeleteTheme",
"async function searchAndSelectMainExpert");
AssertInOrder(
create,
"let added = await addOperatorCatalogStockByDoubleClick(",
"await verifyKrxThemeCreateMarketRoundTrip(",
"duplicate recheck after market round-trip",
"name available after market round-trip",
"stock re-add after market round-trip",
"let expectedSavedRows = 1");
}
[Fact]
public void DryRunProgramParityUsesActualKeysAndTheOriginalEnabledStateSplit()
{
var dryRunAllowed = Slice(
Harness,
": dryRunPlayoutProfile",
": readOnlyAllowedOutboundMessageTypes;");
Assert.Contains("\"set-all-playlist-enabled\"", dryRunAllowed,
StringComparison.Ordinal);
Assert.Contains("\"toggle-active-playlist-enabled\"", dryRunAllowed,
StringComparison.Ordinal);
Assert.DoesNotContain("\"set-playlist-enabled\"", dryRunAllowed,
StringComparison.Ordinal);
var authorization = Slice(
Harness,
"function authorizeDryRunPlayoutShortcut",
"async function setSearchText");
AssertInOrder(
authorization,
"new Set([\"F8\", \"Escape\"])",
"const expectedPhase = definitionName === \"F8\" ? \"idle\" : \"program\"",
"playout.isCommandAvailable !== true",
"playout.outcomeUnknown === true",
"playout.isPlayCompletionPending === true",
"playout.isTakeOutCompletionPending === true",
"authorization.consumed === true",
"Date.now() - authorization.issuedAt > 5_000",
"authorization.consumed = true");
var sequence = Slice(
Harness,
"async function executeDryRunPlayoutSequence",
"function recordFullUiDbCheck");
AssertInOrder(
sequence,
"const f8Authorization = authorizeDryRunPlayoutShortcut(",
"\"rapid DryRun F8 after row selection\"",
"PROGRAM clear every enabled row",
"snapshot.state.playlist.every(row => row.isEnabled === false)",
"snapshot.state.playout.nextKind === \"endOfPlaylist\"",
"PROGRAM restore every enabled row",
"snapshot.state.playlist.every(row => row.isEnabled === true)",
"snapshot.state.playout.nextKind === \"playlistNext\"",
"pointerClickDisabledPlaylistCheckbox(",
"checkboxAriaDisabled === true",
"pressKey(\"Space\"",
"[\"toggle-active-playlist-enabled\"]",
"DryRun NEXT skips the Space-disabled future row",
"currentEntryId === nextEntryId",
"const escapeAuthorization = authorizeDryRunPlayoutShortcut(",
"\"DryRun Escape TAKE OUT after NEXT\"",
"snapshot.state.playout.phase === \"idle\"");
Assert.Contains("[\"set-all-playlist-enabled\"]", sequence,
StringComparison.Ordinal);
Assert.Contains("mouseCheckboxDisabled: true", sequence,
StringComparison.Ordinal);
Assert.Contains("setAllIntentCount: 2", sequence,
StringComparison.Ordinal);
Assert.Contains("toggleActiveIntentCount: 1", sequence,
StringComparison.Ordinal);
}
[Fact]
public void CatalogBlankStockSearchUsesButtonAndEnterAndSealsTheBoundedDisplay()
{
var boundedSearch = Slice(
Harness,
"async function verifyBlankOperatorCatalogStockSearch",
"async function addOperatorCatalogStock");
AssertInOrder(
boundedSearch,
"replaceText(",
"`${label} blank query`",
"trigger === \"button\"",
"operator-catalog-stock-search",
"pressKey(\"Enter\"",
"currentCatalog?.stockQuery === \"\"",
"resultCount > 1 && resultCount <= 10_000",
"statusMessage === \"종목 검색을 완료했습니다.\"",
"operatorCatalogStockResultCount === resultCount",
"[\"search-operator-catalog-stocks\"]",
"messages[0].emptyStockQuery !== true",
"resultCount !== 10_000 || !truncationVisible",
"draftRowsPreserved: true");
var theme = Slice(
Harness,
"async function createReadEditDeleteTheme",
"async function searchAndSelectMainExpert");
AssertInOrder(
theme,
"`${label} ThemeA editor`",
"addOperatorCatalogStockByDoubleClick(");
var expert = Slice(
Harness,
"async function createReadEditDeleteExpert",
"async function executeOperatorCatalogCrud");
AssertInOrder(
expert,
"snapshot.state.operatorCatalog?.mode === \"edit\"",
"`${label} existing UC6 editor`",
"addOperatorCatalogStockByDoubleClick(");
}
[Fact]
public void PhysicalPlaylistExchangeShiftSelectsDeletesAndRestoresTheExactThreeRows()
{
var request = Slice(
Harness,
"async function requestWindowsInput(",
"async function installProbe");
AssertInOrder(
request,
"beforeOrder.length !== 5",
"reorderedOrder.at(-2) !== rangeStartRowId",
"const afterOrder = reorderedOrder.filter",
"row-header-drag-home-shift-range-delete-restore",
"expectedRangeSelectedRowIds: [rangeStartRowId, rangeEndRowId]",
"mouseDownCalls: 4",
"shiftKeyDownCalls: 1",
"shiftKeyUpCalls: 1",
"deleteKeyCalls: 1",
"delete-selected-playlist-rows",
"selections[1].shift !== true",
"shiftRangeDeletedRowIds = [rangeStartRowId, rangeEndRowId]");
Assert.DoesNotContain("Input.dispatchMouseEvent", request,
StringComparison.Ordinal);
var native = Slice(
Runner,
"public static void SendShiftClickRangeThenDeleteAndRestore(",
"public static void SendApplicationClick(");
AssertInOrder(
native,
"SendKeyboard(VirtualKeyShift, false)",
"SendMouseButton(MouseLeftDown)",
"SendKeyboard(VirtualKeyShift, true)",
"SendKeyboard(VirtualKeyDelete, false)",
"SendKeyboard(VirtualKeyDelete, true)",
"SendMouseMove(restore.X, restore.Y)");
}
[Fact]
public void FullUiDbPListUsesASecondPhysicalDoubleClickExchangeWithContentRestoration()
{
var request = Slice(
Harness,
"async function requestNamedPlaylistLoadDoubleClick",
"function namedPlaylistDeleteWriteCount");
AssertInOrder(
request,
"configuration.windowsInputExchangeCount !== 2",
"DB 재생목록 불러오기",
"const beforeContent = playlistContentProjection(current)",
"named-playlist-load-double-click",
"mouseDownCalls: 2",
"doubleClickIntervalMilliseconds: 100",
"load-named-playlist-by-id",
"JSON.stringify(playlistContentProjection(snapshot))",
"loads.length !== 1",
"approvedDatabaseWriteMessages.length !== beforeApprovedWriteCount",
"contentRestored: true");
Assert.DoesNotContain("pointerDoubleClick", request,
StringComparison.Ordinal);
Assert.DoesNotContain("Input.dispatchMouseEvent", request,
StringComparison.Ordinal);
var runnerExchange = Slice(
Runner,
"function Get-ExpectedWindowsInputExchangeCount",
"function Invoke-NodeHarnessUnderTcpWatch");
AssertInOrder(
runnerExchange,
"return 2",
"named-playlist-load-double-click",
"Invoke-ExactNamedPlaylistDoubleClickRequest",
"Write-WindowsInputAcknowledgement");
Assert.Contains("public static void SendApplicationDoubleClick(", Runner,
StringComparison.Ordinal);
Assert.Contains("for (int click = 0; click < 2; click++)", Runner,
StringComparison.Ordinal);
Assert.Contains("PList physical double-click fresh readback", Runner,
StringComparison.Ordinal);
}
[Fact]
public void DebugAppXMustMatchTheLatestMsixAndCurrentWebSourceBeforeLaunch()
{
var guard = Slice(
Runner,
"function Assert-DebugAppXMatchesLatestPackage",
"function Get-ExactDevelopmentPackage");
Assert.Contains("$BuildFlavor -cne 'DebugAppX'", guard,
StringComparison.Ordinal);
Assert.Contains("[IO.Compression.ZipFile]::OpenRead($msix)", guard,
StringComparison.Ordinal);
Assert.Contains("'Web/app.js'", guard, StringComparison.Ordinal);
Assert.Contains("Get-StreamSha256Hex $entryStream", guard,
StringComparison.Ordinal);
Assert.Contains("registered Debug AppX is stale", guard,
StringComparison.Ordinal);
Assert.Contains("Web\\app.js", guard, StringComparison.Ordinal);
Assert.Contains(
"Assert-DebugAppXMatchesLatestPackage ([string]$package.InstallLocation)",
Runner,
StringComparison.Ordinal);
}
private static string Slice(string source, string start, string end)
{
var startIndex = source.IndexOf(start, StringComparison.Ordinal);
var endIndex = source.IndexOf(end, startIndex, StringComparison.Ordinal);
Assert.True(startIndex >= 0 && endIndex > startIndex);
return source[startIndex..endIndex];
}
private static void AssertInOrder(string source, params string[] markers)
{
var cursor = 0;
foreach (var marker in markers)
{
var index = source.IndexOf(marker, cursor, StringComparison.Ordinal);
Assert.True(index >= cursor, $"Marker was missing or out of order: {marker}");
cursor = index + marker.Length;
}
}
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,165 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyPackageLocalStateSmokeContractTests
{
private static readonly string Root = FindRepositoryRoot();
private static readonly string Wrapper = Read(
"scripts", "Invoke-LegacyPackageLocalStateSmoke.ps1");
private static readonly string Harness = Read(
"scripts", "Test-LegacyPackageLocalStateSmoke.mjs");
private static readonly string Shared = Read(
"scripts", "Invoke-LegacyPackageInputSmoke.ps1");
[Fact]
public void Dedicated_profile_reuses_exact_debug_package_and_dryrun_guards()
{
Assert.Contains("-BuildFlavor DebugAppX", Wrapper, StringComparison.Ordinal);
Assert.Contains("-DefinitionsOnly", Wrapper, StringComparison.Ordinal);
Assert.Contains("[string]$LegacyDataDirectory", Wrapper, StringComparison.Ordinal);
Assert.Contains(
"'..\\MBN_STOCK_N\\MBN_STOCK_N\\bin\\Debug\\Data'",
Wrapper,
StringComparison.Ordinal);
Assert.DoesNotMatch(@"(?i)C:\\Users\\[^\\]+", Wrapper);
Assert.Contains("Assert-LocalConfigurationIsDryRun", Wrapper, StringComparison.Ordinal);
Assert.Contains("Assert-NoTornadoConnection", Wrapper, StringComparison.Ordinal);
Assert.Contains("Get-ExactDevelopmentPackage", Wrapper, StringComparison.Ordinal);
Assert.Contains("[switch]$DefinitionsOnly", Shared, StringComparison.Ordinal);
Assert.Contains("if ($DefinitionsOnly)", Shared, StringComparison.Ordinal);
Assert.Contains("function Read-LocalHarnessEvidenceFile", Wrapper,
StringComparison.Ordinal);
Assert.Contains("$item.Length -gt 128KB", Wrapper, StringComparison.Ordinal);
Assert.DoesNotContain(
"Read-SmallJsonEvidenceFile $EvidencePath 'Local-state harness evidence'",
Wrapper,
StringComparison.Ordinal);
}
[Fact]
public void Folder_pickers_are_real_click_owned_unique_and_escape_cancel_only()
{
Assert.Contains("SendApplicationClick(", Shared, StringComparison.Ordinal);
Assert.Contains("SendEscapeToForegroundWindow", Shared, StringComparison.Ordinal);
Assert.Contains("Key-up only releases the", Shared,
StringComparison.Ordinal);
Assert.Contains("caller separately proves that this exact", Shared,
StringComparison.Ordinal);
Assert.DoesNotContain("lost foreground before Escape was released", Shared,
StringComparison.Ordinal);
Assert.Contains("Wait-UniqueOwnedFolderPicker", Wrapper, StringComparison.Ordinal);
Assert.Contains("OwnerChainContains", Wrapper, StringComparison.Ordinal);
Assert.Contains("VisibleTopLevelWindows", Wrapper, StringComparison.Ordinal);
Assert.Contains("EnumWindows(callback, IntPtr.Zero)", Wrapper,
StringComparison.Ordinal);
Assert.Contains("AutomationElement]::FromHandle($handle)", Wrapper,
StringComparison.Ordinal);
Assert.Contains("$windowClass -cne '#32770'", Wrapper,
StringComparison.Ordinal);
Assert.Contains("$revalidated[0].Handle -ne $picker.Handle", Wrapper,
StringComparison.Ordinal);
Assert.Contains("ControlType]::Button", Wrapper, StringComparison.Ordinal);
Assert.Contains("Current.ClassName -ceq 'Button'", Wrapper,
StringComparison.Ordinal);
Assert.Contains("$automationId -ceq '1'", Wrapper, StringComparison.Ordinal);
Assert.Contains("$automationId -ceq '2'", Wrapper, StringComparison.Ordinal);
Assert.Contains("WindowPattern]::Pattern", Wrapper, StringComparison.Ordinal);
Assert.Contains("$windowPattern.Current.IsModal", Wrapper,
StringComparison.Ordinal);
Assert.Contains("AcquireOwnedForeground", Wrapper, StringComparison.Ordinal);
Assert.Contains("OwnerChainContains(window, expectedOwner)", Wrapper,
StringComparison.Ordinal);
Assert.Contains("uniqueNewPicker = $true", Wrapper, StringComparison.Ordinal);
Assert.Contains("closedAfterEscape = $true", Wrapper, StringComparison.Ordinal);
Assert.Contains("not destroyed after the one Escape input", Wrapper,
StringComparison.Ordinal);
Assert.Contains("waitForTrustedPointerClick", Harness, StringComparison.Ordinal);
Assert.Contains("isTrusted: event.isTrusted === true", Harness,
StringComparison.Ordinal);
Assert.Contains("settings file changed during a cancel-only smoke", Wrapper,
StringComparison.Ordinal);
Assert.DoesNotContain("reset-operator-folder", Harness, StringComparison.Ordinal);
}
[Fact]
public void Comparison_import_requires_exact_source_one_row_baseline_and_idempotence()
{
Assert.Contains(
"1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2",
Wrapper,
StringComparison.Ordinal);
Assert.Contains(
"13C07BA64587549EDA9C1A694F3DFA19875CB1F888F02C9E071F17C47739E087",
Wrapper,
StringComparison.Ordinal);
Assert.Contains("receipt?.addedPairCount === 8", Harness, StringComparison.Ordinal);
Assert.Contains("receipt?.addedPairCount === 0", Harness, StringComparison.Ordinal);
Assert.Contains("receipt?.skippedDuplicateCount === 8", Harness,
StringComparison.Ordinal);
Assert.Equal(
2,
CountOccurrences(
Harness,
"allowBusy: true, allowDialog: true, timeoutMilliseconds: 90_000"));
Assert.Contains("secondImportIdempotent = true", Harness, StringComparison.Ordinal);
Assert.Contains("rowId: pair.rowId", Harness, StringComparison.Ordinal);
Assert.Contains("displayLabel: pair.displayLabel", Harness, StringComparison.Ordinal);
Assert.Contains(
"initialPair.displayLabel !== \"KRX100지수 ↔ 코스피 지수\"",
Harness,
StringComparison.Ordinal);
Assert.DoesNotContain("pair.first?.displayName", Harness, StringComparison.Ordinal);
Assert.DoesNotContain("pair.second?.displayName", Harness, StringComparison.Ordinal);
Assert.Contains("no input was retried", Harness, StringComparison.Ordinal);
}
[Fact]
public void Harness_has_no_cdp_input_or_app_close_call_site()
{
Assert.Contains("\"Input.dispatchMouseEvent\"", Harness, StringComparison.Ordinal);
Assert.DoesNotContain("rpc(\"Input.dispatchMouseEvent\"", Harness,
StringComparison.Ordinal);
Assert.DoesNotContain("rpc(\"Input.dispatchKeyEvent\"", Harness,
StringComparison.Ordinal);
Assert.DoesNotContain("rpc(\"Browser.close\"", Harness, StringComparison.Ordinal);
Assert.DoesNotContain("rpc(\"Page.close\"", Harness, StringComparison.Ordinal);
Assert.DoesNotContain("Stop-Process", Wrapper, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("taskkill", Wrapper, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Initial_state_race_is_input_free_and_stdin_does_not_hold_node_open()
{
Assert.Contains("process.stdin.unref", Harness, StringComparison.Ordinal);
Assert.Contains("label !== \"DryRun IDLE preflight\"", Harness,
StringComparison.Ordinal);
Assert.Contains("Only the input-free initial preflight", Harness,
StringComparison.Ordinal);
Assert.Contains("allowMissingState: true", Harness, StringComparison.Ordinal);
Assert.Contains("last.safety.blockedOutboundMessages.length", Harness,
StringComparison.Ordinal);
Assert.Contains("last.dom.connectionText !== \"DRY RUN\"", Harness,
StringComparison.Ordinal);
}
private static string Read(params string[] segments) =>
File.ReadAllText(Path.Combine([Root, .. segments]));
private static int CountOccurrences(string value, string token) =>
value.Split(token, StringSplitOptions.None).Length - 1;
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

@@ -11,6 +11,8 @@ public sealed class LegacyPlayoutNativeContractTests
Path.Combine(AppRoot, "MainWindow.Playout.cs"));
private static readonly string Window = File.ReadAllText(
Path.Combine(AppRoot, "MainWindow.xaml.cs"));
private static readonly string BackgroundPicker = File.ReadAllText(
Path.Combine(AppRoot, "MainWindow.BackgroundPicker.cs"));
private static readonly string Project = File.ReadAllText(
Path.Combine(AppRoot, "MBN_STOCK_WEBVIEW.LegacyParityApp.csproj"));
@@ -115,6 +117,44 @@ public sealed class LegacyPlayoutNativeContractTests
StringComparison.Ordinal);
}
[Fact]
public void LocalSelectionAndDoubleClickGesturesQueueBehindRefreshInArrivalOrder()
{
var process = Normalize(Slice(
Window,
"private async Task ProcessIntentAsync(LegacyUiIntent intent)",
"private LegacyOperatorSnapshot SelectCutWithoutAppend("));
var classifier = Slice(
Window,
"private static bool IsQueuedLocalSelectionIntent",
"private static bool IsOperatorSettingsIntent");
Assert.Contains("_orderedLocalSelectionIntentGate.WaitAsync(", process,
StringComparison.Ordinal);
Assert.Contains("else if (IsQueuedLocalSelectionIntent(intent))", process,
StringComparison.Ordinal);
Assert.Contains("await _intentGate.WaitAsync(_lifetimeCancellation.Token)", process,
StringComparison.Ordinal);
Assert.Contains("_orderedLocalSelectionIntentGate.Release();", process,
StringComparison.Ordinal);
Assert.Contains("LegacySelectStockIntent", classifier, StringComparison.Ordinal);
Assert.Contains("LegacySelectOperatorCatalogStockIntent", classifier,
StringComparison.Ordinal);
Assert.Contains("LegacyActivateOperatorCatalogStockIntent", classifier,
StringComparison.Ordinal);
Assert.Contains("LegacyCutPointerDownIntent", classifier, StringComparison.Ordinal);
Assert.Contains("LegacyCutKeyDownIntent", classifier, StringComparison.Ordinal);
Assert.Contains("LegacyClearCutSelectionIntent", classifier, StringComparison.Ordinal);
Assert.Contains("LegacySelectPlaylistRowIntent", classifier, StringComparison.Ordinal);
Assert.Contains("LegacyActivatePlaylistRowIntent", classifier, StringComparison.Ordinal);
Assert.Contains("LegacySelectPlaylistBoundaryIntent", classifier, StringComparison.Ordinal);
Assert.DoesNotContain("LegacySearchStocksIntent", classifier, StringComparison.Ordinal);
Assert.DoesNotContain("LegacyDropSelectedCutsIntent", classifier, StringComparison.Ordinal);
Assert.DoesNotContain("LegacyExecutePlayoutIntent", classifier, StringComparison.Ordinal);
Assert.DoesNotContain("LegacySave", classifier, StringComparison.Ordinal);
}
[Fact]
public void AutomaticRefreshUsesTheCoreSchedulerAndGenerationOwnedState()
{
@@ -307,6 +347,96 @@ public sealed class LegacyPlayoutNativeContractTests
StringComparison.Ordinal);
}
[Fact]
public void BackgroundControlsStageTheNextCueWithoutChangingTheOnAirRefreshSource()
{
Assert.Contains("_stagedCompositionOptions", Playout, StringComparison.Ordinal);
Assert.Contains(
"_stagedCompositionOptions = new MutableLegacySceneCueCompositionOptionsSource(\n" +
" initialComposition);",
Normalize(Playout),
StringComparison.Ordinal);
var toggle = Slice(
Playout,
"private async Task<LegacyOperatorSnapshot> ToggleBackgroundAsync(",
"private async Task<LegacyOperatorSnapshot> ChooseBackgroundAsync(");
Assert.Contains("if (!CanStageBackground())", toggle, StringComparison.Ordinal);
Assert.Contains("var source = _stagedCompositionOptions!;", toggle,
StringComparison.Ordinal);
Assert.DoesNotContain("var source = _compositionOptions!;", toggle,
StringComparison.Ordinal);
var stageGuard = Slice(
Playout,
"private bool CanStageBackground()",
"private void ApplyStagedCompositionForNextCue()");
Assert.Contains("status.State != PlayoutConnectionState.OutcomeUnknown", stageGuard,
StringComparison.Ordinal);
Assert.Contains("!status.IsPlayCompletionPending", stageGuard,
StringComparison.Ordinal);
Assert.Contains("!status.IsTakeOutCompletionPending", stageGuard,
StringComparison.Ordinal);
Assert.DoesNotContain("PreparedSceneName", stageGuard, StringComparison.Ordinal);
Assert.DoesNotContain("OnAirSceneName", stageGuard, StringComparison.Ordinal);
Assert.DoesNotContain("refresh.IsActive", stageGuard, StringComparison.Ordinal);
var execute = Slice(
Playout,
"private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(",
"private async Task<LegacyOperatorPlayoutRequest> CreateFreshPagedPlayoutRequestAsync(");
var stopRefresh = RequiredIndex(execute, "StopRefreshLoop();");
var promote = RequiredIndex(execute, "ApplyStagedCompositionForNextCue();");
var dispatch = RequiredIndex(execute, "switch (command)");
Assert.True(stopRefresh < promote && promote < dispatch);
Assert.Contains("LegacyOperatorPlayoutCommand.Prepare or", execute,
StringComparison.Ordinal);
Assert.Contains("LegacyOperatorPlayoutCommand.TakeIn or", execute,
StringComparison.Ordinal);
Assert.Contains("LegacyOperatorPlayoutCommand.Next", execute,
StringComparison.Ordinal);
var promotion = Slice(
Playout,
"private void ApplyStagedCompositionForNextCue()",
"private bool CanAcceptOperatorMutation(");
Assert.Contains("active.Update(staged.Current);", promotion,
StringComparison.Ordinal);
Assert.Contains("var composition = _stagedCompositionOptions?.Current", Playout,
StringComparison.Ordinal);
Assert.Contains("CanStageBackground(),", Playout, StringComparison.Ordinal);
}
[Fact]
public void F2PickerStartsInsideTheValidatedBackgroundRootAndKeepsClosedExtensions()
{
var choose = Slice(
Playout,
"private async Task<LegacyOperatorSnapshot> ChooseBackgroundAsync(",
"private bool CanChangeComposition()");
Assert.Contains("TryGetTrustedBackgroundDirectory(", choose,
StringComparison.Ordinal);
Assert.Contains("PickBackgroundFileAsync(\n trustedBackgroundDirectory)",
Normalize(choose), StringComparison.Ordinal);
Assert.DoesNotContain("PickerLocationId.PicturesLibrary", Playout,
StringComparison.Ordinal);
Assert.Contains("dialog.SetFolder(initialFolder);", BackgroundPicker,
StringComparison.Ordinal);
Assert.Contains("dialog.SetDefaultFolder(initialFolder);", BackgroundPicker,
StringComparison.Ordinal);
Assert.Contains("*.vrv;*.jpg;*.jpeg;*.png", BackgroundPicker,
StringComparison.Ordinal);
Assert.Contains("FileOpenOptions.ForceFileSystem", BackgroundPicker,
StringComparison.Ordinal);
Assert.Contains("FileOpenOptions.NoDereferenceLinks", BackgroundPicker,
StringComparison.Ordinal);
Assert.Contains("WindowNative.GetWindowHandle(this)", BackgroundPicker,
StringComparison.Ordinal);
Assert.Contains("DispatcherQueue.HasThreadAccess", BackgroundPicker,
StringComparison.Ordinal);
}
[Fact]
public void HomeAndEndStopAutomaticRefreshBeforeChangingTheCandidateRow()
{
@@ -334,9 +464,23 @@ public sealed class LegacyPlayoutNativeContractTests
}
[Fact]
public void PreparedProgramBlocksCutAppendButAllowsPureCutSelection()
public void ProgramAllowsOriginalEnabledGesturesAndSafeTailAppendButKeepsStructuralEditsLocked()
{
Assert.Contains("CanAcceptOperatorMutation()", Window, StringComparison.Ordinal);
Assert.Contains("CanAcceptOperatorMutation(intent, out var mutationRejectionMessage)",
Window, StringComparison.Ordinal);
Assert.Contains("CanAcceptOperatorMutation(", Playout, StringComparison.Ordinal);
Assert.Contains("RequiresIdlePlaylistMutation(intent)", Playout,
StringComparison.Ordinal);
Assert.Contains("CanDeleteSelectedPlaylistTail(status, workflow)", Playout,
StringComparison.Ordinal);
Assert.Contains("isSafeCutTailAppend", Playout, StringComparison.Ordinal);
Assert.Contains("isSafeProgramEnabledStateMutation", Playout,
StringComparison.Ordinal);
Assert.Contains("refreshAllowsMutation", Playout, StringComparison.Ordinal);
Assert.Contains("!status.IsPlayCompletionPending", Playout, StringComparison.Ordinal);
Assert.Contains("!status.IsTakeOutCompletionPending", Playout, StringComparison.Ordinal);
Assert.Contains("status.State != PlayoutConnectionState.OutcomeUnknown", Playout,
StringComparison.Ordinal);
Assert.Contains("IsOperatorMutationQuarantined()", Window,
StringComparison.Ordinal);
Assert.Contains("ObserveOperatorMutationQuarantine(state)", Window,
@@ -351,6 +495,44 @@ public sealed class LegacyPlayoutNativeContractTests
StringComparison.Ordinal);
Assert.Contains("LegacyConfirmManualListIntent", Playout,
StringComparison.Ordinal);
Assert.Contains("CanMutatePlaylistEnabledState(status, workflow)", Playout,
StringComparison.Ordinal);
var idleOnlyMutations = Slice(
Playout,
"private static bool RequiresIdlePlaylistMutation",
"private static bool IsPlaylistMutationIdle");
Assert.Contains("LegacySetPlaylistEnabledIntent", idleOnlyMutations,
StringComparison.Ordinal);
Assert.DoesNotContain("LegacySetAllPlaylistEnabledIntent", idleOnlyMutations,
StringComparison.Ordinal);
Assert.DoesNotContain("LegacyToggleActivePlaylistEnabledIntent", idleOnlyMutations,
StringComparison.Ordinal);
var programEnabledAdmission = Slice(
Playout,
"private bool CanMutatePlaylistEnabledState(",
"private static bool IsOperatorMutationIntent");
Assert.Contains("status.OnAirSceneName", programEnabledAdmission,
StringComparison.Ordinal);
Assert.Contains("workflow.OnAirCutCode", programEnabledAdmission,
StringComparison.Ordinal);
Assert.Contains("workflow.CurrentEntryId", programEnabledAdmission,
StringComparison.Ordinal);
Assert.Contains("workflow.CurrentCueIndexZeroBased", programEnabledAdmission,
StringComparison.Ordinal);
Assert.Contains("IsPlaylistMutationIdle(status, workflow)", programEnabledAdmission,
StringComparison.Ordinal);
var nextKindProjection = Slice(
Playout,
"private LegacyWorkflowNextKind ResolveOperatorNextKind(",
"private LegacyOperatorPlayoutSnapshot CreatePlayoutSnapshot()");
Assert.Contains("workflow.IsLastPage", nextKindProjection,
StringComparison.Ordinal);
Assert.Contains("LegacyWorkflowNextKind.PageNext", nextKindProjection,
StringComparison.Ordinal);
Assert.Contains("Skip(currentIndex + 1)", nextKindProjection,
StringComparison.Ordinal);
Assert.Contains("Any(static row => row.IsEnabled)", nextKindProjection,
StringComparison.Ordinal);
var mutationClassifier = Slice(
Playout,
"private static bool IsOperatorMutationIntent",
@@ -363,6 +545,15 @@ public sealed class LegacyPlayoutNativeContractTests
StringComparison.Ordinal);
Assert.Contains("pointer.AllowAppend", Window,
StringComparison.Ordinal);
Assert.Equal(2, Window.Split(
"PostState(SelectCutWithoutAppend(cutPointer));",
StringSplitOptions.None).Length - 1);
var selectionOnly = Slice(
Window,
"private LegacyOperatorSnapshot SelectCutWithoutAppend(",
"private static bool IsOrderedPlayoutIntent");
Assert.Contains("allowAppend: false", selectionOnly,
StringComparison.Ordinal);
}
private static void AssertKnownFailureDoesNotQuarantine(

View File

@@ -0,0 +1,104 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyRovingListParityWebTests
{
private static readonly string RepositoryRoot = FindRepositoryRoot();
private static readonly string App = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"Web",
"app.js"));
[Fact]
public void Restored_large_lists_share_one_roving_tab_contract()
{
foreach (var selector in new[]
{
"button[data-comparison-selection-id]",
"button[data-comparison-target-id]",
"button[data-comparison-pair-id]",
"button[data-theme-result-id]",
"button[data-expert-selection-id]",
"button[data-halt-selection-id]",
"button[data-manual-result-id]",
"button[data-manual-stock-result-id]",
"button[data-manual-vi-result-id]",
"button[data-named-playlist-definition-id]",
"button[data-operator-catalog-result-id]",
"button[data-operator-catalog-stock-result-id]"
})
{
Assert.Contains(selector, App,
StringComparison.Ordinal);
}
Assert.Contains("function configureRovingList", App, StringComparison.Ordinal);
Assert.Contains("row.tabIndex = row === tabStop ? 0 : -1;", App,
StringComparison.Ordinal);
Assert.Contains("list.tabIndex = tabStop ? -1 : 0;", App,
StringComparison.Ordinal);
Assert.Contains("settings.rowRole || \"option\"", App,
StringComparison.Ordinal);
Assert.Contains("settings.listRole || \"listbox\"", App,
StringComparison.Ordinal);
Assert.Contains("configureTreeRovingLists", App, StringComparison.Ordinal);
Assert.Contains("listRole: \"tree\"", App, StringComparison.Ordinal);
Assert.Contains("rowRole: \"treeitem\"", App, StringComparison.Ordinal);
Assert.Contains("activation: \"focus\"", App, StringComparison.Ordinal);
}
[Fact]
public void Roving_navigation_is_IME_safe_and_sends_only_closed_selection_intents()
{
var start = App.IndexOf("function activateRovingListRow", StringComparison.Ordinal);
var end = App.IndexOf("document.addEventListener(\"focusin\"", start,
StringComparison.Ordinal);
Assert.True(start >= 0 && end > start);
var activation = App[start..end];
foreach (var intent in new[]
{
"select-comparison-domestic",
"select-comparison-world",
"select-comparison-fixed",
"select-comparison-pair",
"select-expert",
"select-trading-halt",
"load-manual-financial",
"select-manual-vi-result"
})
{
Assert.Contains($"\"{intent}\"", activation, StringComparison.Ordinal);
}
Assert.DoesNotContain("choose-comparison", activation, StringComparison.Ordinal);
Assert.DoesNotContain("activate-", activation, StringComparison.Ordinal);
Assert.Contains("isRovingListNavigationKey(event.key)", App,
StringComparison.Ordinal);
Assert.Contains("!isImeComposing(event)", App, StringComparison.Ordinal);
Assert.Contains("event.key === \"ArrowUp\"", App, StringComparison.Ordinal);
Assert.Contains("event.key === \"ArrowDown\"", App, StringComparison.Ordinal);
Assert.Contains("event.key === \"Home\"", App, StringComparison.Ordinal);
Assert.Contains("event.key === \"End\"", App, StringComparison.Ordinal);
Assert.Contains("key === \"PageUp\"", App, StringComparison.Ordinal);
Assert.Contains("key === \"PageDown\"", App, StringComparison.Ordinal);
Assert.Contains("rovingListPageSize", App, 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

@@ -58,14 +58,17 @@ public sealed class LegacyRowDoubleClickParityTests
Assert.Contains("clearDelayedClick(themeResultClickTimer)", doubleClick,
StringComparison.Ordinal);
Assert.Contains(
"send(\"select-theme\", { resultId: resultId });",
"if (send(\"select-theme\", { resultId: resultId }))",
themeClick,
StringComparison.Ordinal);
Assert.Contains("beginThemeResultPendingSelection(themeResult)", themeClick,
StringComparison.Ordinal);
Assert.Contains("clearThemeResultPendingSelection(true)", themeClick,
StringComparison.Ordinal);
Assert.Contains("clearDelayedClick(themeResultClickTimer)", themeClick,
StringComparison.Ordinal);
Assert.Contains("themeResultClickTimer = window.setTimeout", themeClick,
Assert.Contains("themeResultClickTimer = deferLegacySingleClick", themeClick,
StringComparison.Ordinal);
Assert.Contains("}, 250);", themeClick, StringComparison.Ordinal);
}
[Fact]

View File

@@ -49,6 +49,109 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
Assert.DoesNotContain("afiedt.buf.txt", packagedResNames);
}
[Fact]
public void GraphEPreviewsExposeOnlyFourAllowlistedBitmapsThroughTheTrustedWebRoot()
{
var project = XDocument.Load(ProjectPath);
var previewNames = project.Descendants("LegacyFinancialPreviewAsset")
.Select(element => Path.GetFileName(element.Attribute("Include")?.Value)!)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
Assert.Equal(
new HashSet<string>(
["pie.bmp", "Grow.bmp", "sell.bmp", "profit.bmp"],
StringComparer.OrdinalIgnoreCase),
previewNames);
var previewContent = Assert.Single(
project.Descendants("Content"),
element => string.Equals(
element.Attribute("Include")?.Value,
"@(LegacyFinancialPreviewAsset)",
StringComparison.Ordinal));
Assert.Equal(
"Web\\Previews\\%(Filename)%(Extension)",
previewContent.Element("Link")?.Value);
Assert.Equal(
"Web\\Previews\\%(Filename)%(Extension)",
previewContent.Element("TargetPath")?.Value);
Assert.Equal(
"PreserveNewest",
previewContent.Element("CopyToOutputDirectory")?.Value);
Assert.Equal(
"PreserveNewest",
previewContent.Element("CopyToPublishDirectory")?.Value);
Assert.DoesNotContain(
project.Descendants("Content"),
element => string.Equals(
element.Attribute("Include")?.Value,
"$(LegacyResSourceRoot)\\**\\*",
StringComparison.OrdinalIgnoreCase));
var window = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.xaml.cs"));
Assert.Equal(
1,
window.Split(
"SetVirtualHostNameToFolderMapping(",
StringSplitOptions.None).Length - 1);
Assert.Contains("LegacyBridgeProtocol.TrustedHost", window, StringComparison.Ordinal);
Assert.Contains("webRoot", window, StringComparison.Ordinal);
Assert.DoesNotContain(
"SetVirtualHostNameToFolderMapping(\"res",
window,
StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void WebViewIgnoresItsPersistentCacheBeforeExactTrustedNavigation()
{
var window = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.xaml.cs"));
var mapping = window.IndexOf(
"SetVirtualHostNameToFolderMapping(",
StringComparison.Ordinal);
var disableCache = window.IndexOf(
"\"Network.setCacheDisabled\"",
StringComparison.Ordinal);
var navigation = window.IndexOf(
"Browser.Source = new Uri(",
StringComparison.Ordinal);
Assert.True(mapping >= 0);
Assert.True(disableCache > mapping);
Assert.True(navigation > disableCache);
Assert.Contains(
"CoreWebView2HostResourceAccessKind.DenyCors",
window,
StringComparison.Ordinal);
Assert.Equal(
1,
window.Split(
"\"Network.setCacheDisabled\"",
StringSplitOptions.None).Length - 1);
Assert.Contains(
"\"{\\\"cacheDisabled\\\":true}\"",
window,
StringComparison.Ordinal);
Assert.Contains(
"$\"https://{LegacyBridgeProtocol.TrustedHost}/index.html\"",
window,
StringComparison.Ordinal);
Assert.DoesNotContain(
"$\"https://{LegacyBridgeProtocol.TrustedHost}/index.html?",
window,
StringComparison.Ordinal);
}
[Fact]
public void ActiveDatabaseIniIsPostBuildOutputOnlyAndNeverAPackagingItem()
{
@@ -122,6 +225,12 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
Assert.Equal(
"_ComputeAppxPackagePayload",
rejectionTarget.Attribute("BeforeTargets")?.Value);
var forbiddenPayload = Assert.Single(
rejectionTarget.Descendants("_ForbiddenLegacyDatabasePackagePayload"));
Assert.Contains(
"%(PackagingOutputs.Filename)%(PackagingOutputs.Extension)",
forbiddenPayload.Attribute("Condition")?.Value,
StringComparison.Ordinal);
Assert.Single(rejectionTarget.Elements("Error"));
}
@@ -154,7 +263,8 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
"$(LegacyRuntimeSourceRoot)",
"$(LegacyCutsSourceRoot)",
"$(LegacyResSourceRoot)",
"$(LegacyCutsSourceRoot)\\5001.t2s",
"$(LegacyCutsSourceRoot)\\%(LegacyRequiredScene.Identity).t2s",
"$(LegacyCutsSourceRoot)\\%(LegacyRequiredBuiltInAsset.Identity)",
"$(LegacyResSourceRoot)\\MmoneyCoder.ini",
"$(LegacyResSourceRoot)\\종목.ini",
"$(LegacyResSourceRoot)\\업종_코스피.ini",
@@ -168,6 +278,27 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
Assert.Contains(conditions, condition =>
condition!.Contains(sentinel, StringComparison.Ordinal)));
var requiredScenes = Assert.Single(project.Descendants("LegacyRequiredScene"))
.Attribute("Include")!.Value
.Split(';', StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(45, requiredScenes.Length);
Assert.Equal(requiredScenes.Length,
requiredScenes.Distinct(StringComparer.Ordinal).Count());
Assert.Contains("5001", requiredScenes);
Assert.Contains("5076", requiredScenes);
Assert.Contains("5079", requiredScenes);
Assert.Contains("6001", requiredScenes);
var requiredAssets = Assert.Single(
project.Descendants("LegacyRequiredBuiltInAsset"))
.Attribute("Include")!.Value
.Split(';', StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(9, requiredAssets.Length);
Assert.Contains(@"images\주유기merge.png", requiredAssets);
Assert.Contains(@"images\35752913_l.jpg", requiredAssets);
Assert.Contains(@"Images\KRX.png", requiredAssets);
Assert.Contains(@"Images\NXT.png", requiredAssets);
var databaseIniError = Assert.Single(
target.Elements("Error"),
error => (error.Attribute("Condition")?.Value ?? string.Empty)

View File

@@ -108,7 +108,8 @@ public sealed class LegacyThemeWebParityTests
StringComparison.Ordinal);
Assert.Contains("return;", selectionReceipt, StringComparison.Ordinal);
Assert.DoesNotContain("replaceChildren", selectionReceipt, StringComparison.Ordinal);
Assert.Contains("document.activeElement !== input", updater, StringComparison.Ordinal);
Assert.Contains("syncSearchDraft(input, theme.query)", updater,
StringComparison.Ordinal);
Assert.Contains("button.classList.toggle(\"selected\", selected);", updater,
StringComparison.Ordinal);
Assert.Contains("button.setAttribute(\"aria-selected\"", updater,

View File

@@ -57,7 +57,7 @@ public sealed class LegacyWebContractTests
@"class=""[^""]*\bactive\b[^""]*""[^>]*data-tab-id=""overseas""[\s\S]*?<span class=""workspace-nav-label"">해외<\/span>[\s\S]*?<\/button>",
Markup);
Assert.Matches(
@"data-tab-id=""tradingHalt""[\s\S]*?<span class=""workspace-nav-label"">정지<\/span>[\s\S]*?<\/button>",
@"data-tab-id=""tradingHalt""[\s\S]*?<span class=""workspace-nav-label"">거래정지<\/span>[\s\S]*?<\/button>",
Markup);
}
@@ -124,10 +124,15 @@ public sealed class LegacyWebContractTests
var boundaryStart = App.IndexOf(
"function selectPlaylistBoundary", StringComparison.Ordinal);
var boundaryEnd = App.IndexOf(
"function clearDelayedClick", boundaryStart, StringComparison.Ordinal);
"function movePlaylistKeyboardSelection", boundaryStart, StringComparison.Ordinal);
var boundaryHelper = App[boundaryStart..boundaryEnd];
Assert.DoesNotContain(".focus(", boundaryHelper, StringComparison.Ordinal);
Assert.Contains("scrollIntoView", boundaryHelper, StringComparison.Ordinal);
var arrowEnd = App.IndexOf(
"function clearDelayedClick", boundaryEnd, StringComparison.Ordinal);
var arrowHelper = App[boundaryEnd..arrowEnd];
Assert.Contains("next.focus({ preventScroll: true })", arrowHelper,
StringComparison.Ordinal);
}
[Fact]
@@ -152,12 +157,15 @@ public sealed class LegacyWebContractTests
Assert.Contains("event.key === \"Delete\"", App, StringComparison.Ordinal);
Assert.Contains("row.isSelected", App, StringComparison.Ordinal);
Assert.Contains("row.isActive === true", App, StringComparison.Ordinal);
Assert.Contains("enabled.checked !== true", App, StringComparison.Ordinal);
Assert.Contains("enabled.setAttribute(\"aria-disabled\", playlistMutationLocked", App,
Assert.Contains("send(\"toggle-active-playlist-enabled\", {})", App,
StringComparison.Ordinal);
Assert.Contains("interactionDisabled = playlistMutationLocked", App,
Assert.Contains("enabled.setAttribute(\"aria-disabled\", playlistStructureMutationLocked", App,
StringComparison.Ordinal);
Assert.DoesNotContain("send(\"toggle-active-playlist-enabled\"", App,
Assert.Contains("interactionDisabled = playlistStructureMutationLocked", App,
StringComparison.Ordinal);
Assert.Contains("playlistEnableAll.disabled = playlistProgramEnableMutationLocked", App,
StringComparison.Ordinal);
Assert.Contains("playlistDeleteSelected.disabled = playlistDeleteSelectedLocked", App,
StringComparison.Ordinal);
Assert.Contains("deferSelectedRowPlainClickForDrag", App, StringComparison.Ordinal);
Assert.Contains("deferredPlaylistPlainSelectionRowId", App, StringComparison.Ordinal);
@@ -204,7 +212,7 @@ public sealed class LegacyWebContractTests
StringComparison.Ordinal);
Assert.Contains(".playlist-data-row .playlist-row-header", Styles,
StringComparison.Ordinal);
Assert.Contains("if (!playlistMutationLocked) send(\"delete-selected-playlist-rows\"", App,
Assert.Contains("if (!playlistDeleteSelectedLocked) send(\"delete-selected-playlist-rows\"", App,
StringComparison.Ordinal);
var deleteBranchStart = App.LastIndexOf(
"if (event.key === \"Delete\"",
@@ -281,7 +289,7 @@ public sealed class LegacyWebContractTests
}
[Fact]
public void FixedCatalogUsesOpaqueCSharpActionAndSectionIntents()
public void FixedCatalogUsesOpaqueLeafAndParentBatchIntents()
{
Assert.Contains("state.fixedCatalog", App, StringComparison.Ordinal);
Assert.Contains("dataset.actionId", App, StringComparison.Ordinal);
@@ -428,7 +436,7 @@ public sealed class LegacyWebContractTests
"beginNamedPlaylistSelection(definition.dataset.namedPlaylistDefinitionId)", App,
StringComparison.Ordinal);
Assert.Contains(
"dbLoadButton.disabled = operatorMutationLocked || busy || !named",
"dbLoadButton.disabled = playlistStructureMutationLocked || busy || !named",
App,
StringComparison.Ordinal);
Assert.Contains("requestId: requestId", App, StringComparison.Ordinal);

View File

@@ -68,10 +68,10 @@ public sealed class LegacyWorkspaceLayoutContractTests
}
[Fact]
public void WorkAndScheduleUseResponsiveTwoToOneColumns()
public void WorkAndScheduleGiveTheAlwaysVisibleScheduleAWiderResponsiveShare()
{
Assert.Contains(
"grid-template-columns: minmax(900px, 2fr) minmax(600px, 1fr);",
"grid-template-columns: minmax(900px, 1.75fr) minmax(600px, 1fr);",
Styles,
StringComparison.Ordinal);
Assert.Contains(
@@ -139,6 +139,13 @@ public sealed class LegacyWorkspaceLayoutContractTests
Assert.Contains(".playout-actions .take-in:not(:disabled)", Styles, StringComparison.Ordinal);
Assert.Contains(".playout-actions .next:not(:disabled)", Styles, StringComparison.Ordinal);
Assert.Contains(".playout-actions .take-out:not(:disabled)", Styles, StringComparison.Ordinal);
Assert.Contains(
"Contemporary density refinement: keep operator geometry, clarify visual hierarchy",
Styles,
StringComparison.Ordinal);
Assert.Contains(".stock-result.selected", Styles, StringComparison.Ordinal);
Assert.Contains(".playlist-heading-count", Styles, StringComparison.Ordinal);
Assert.Contains(".playlist-rows:empty::after", Styles, StringComparison.Ordinal);
Assert.Contains(
".playlist-data-row[data-active=\"true\"]:not(.on-air)",
Styles,
@@ -169,6 +176,16 @@ public sealed class LegacyWorkspaceLayoutContractTests
@"class=""playlist-heading-icon""[\s\S]*?<svg[^>]*viewBox=""0 0 24 24""",
RegexOptions.CultureInvariant),
Markup);
Assert.Matches(
new Regex(
@"id=""playlist-heading-count""[^>]*role=""status""[^>]*aria-live=""polite""[^>]*aria-atomic=""true""",
RegexOptions.CultureInvariant),
Markup);
Assert.Contains("const nextHeadingCount = `", App, StringComparison.Ordinal);
Assert.Contains(
"if (playlistHeadingCount.textContent !== nextHeadingCount)",
App,
StringComparison.Ordinal);
}
[Fact]
@@ -224,6 +241,16 @@ public sealed class LegacyWorkspaceLayoutContractTests
Assert.Contains("menuButton.click()", Navigation, StringComparison.Ordinal);
Assert.Contains("pendingKeyboardMarketTabId", Navigation, StringComparison.Ordinal);
Assert.Contains("pendingButton.focus()", Navigation, StringComparison.Ordinal);
Assert.Contains("window.requestAnimationFrame(function", Navigation,
StringComparison.Ordinal);
Assert.Contains("options.authoritativeNativeRender === true", Navigation,
StringComparison.Ordinal);
Assert.Contains("hasPendingKeyboardMarketFocus", Navigation,
StringComparison.Ordinal);
Assert.Contains("document.addEventListener(\"pointerdown\"", Navigation,
StringComparison.Ordinal);
Assert.DoesNotContain("catalogWorkspace.contains(active)", Navigation,
StringComparison.Ordinal);
Assert.Contains("LegacyWorkspaceNavigation", Navigation, StringComparison.Ordinal);
Assert.Contains("nav-collapsed", Navigation, StringComparison.Ordinal);
Assert.DoesNotContain("postMessage", Navigation, StringComparison.Ordinal);