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