Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyPackageInputSmokeContractTests.cs

895 lines
37 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 NamedPlaylistDoubleClickSealUsesAsciiSafeUtf8Constants()
{
Assert.Contains("$ExpectedSavedFixtureGraphicType = $Utf8.GetString(", Runner,
StringComparison.Ordinal);
Assert.Contains("'MeyXtO2MkOq4sOuzuA=='", Runner,
StringComparison.Ordinal);
Assert.Contains("'7ZiE7J6s6rCA'", Runner, StringComparison.Ordinal);
Assert.Contains("'7Iuc6rCE7Jm464uo7J286rCA'", Runner,
StringComparison.Ordinal);
var doubleClick = Slice(
Runner,
"function Invoke-ExactNamedPlaylistDoubleClickRequest",
"function Get-BackgroundPickerWindowHandles");
Assert.Contains("$ExpectedSavedFixtureGraphicType", doubleClick,
StringComparison.Ordinal);
Assert.Contains("$ExpectedSavedFixtureCurrentSubtype", doubleClick,
StringComparison.Ordinal);
Assert.Contains("$ExpectedSavedFixtureAfterHoursSubtype", doubleClick,
StringComparison.Ordinal);
Assert.DoesNotContain("'1열판기본'", doubleClick,
StringComparison.Ordinal);
}
[Fact]
public void PhysicalPointerInputsCaptureGeometryAfterForegroundAcquisition()
{
foreach (var (start, end) in new[]
{
("public static void SendRowHeaderDragAfterThenHome(",
"public static void SendShiftClickRangeThenDeleteAndRestore("),
("public static void SendShiftClickRangeThenDeleteAndRestore(",
"public static void SendApplicationClick("),
("public static void SendApplicationClick(",
"public static void SendApplicationDoubleClick("),
("public static void SendApplicationDoubleClick(",
"public static IntPtr ReadForegroundWindow()")
})
{
var input = Slice(Runner, start, end);
AssertInOrder(
input,
"AcquireForeground(window, processId, forbiddenPort);",
"ClientGeometry baseline = ReadAndAssertClientGeometry(",
"SleepWithWindowGuard(");
}
}
[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 ReadOnlyPhysicalPlaylistButtonsAndCheckboxUseTheClosedLocalAllowlist()
{
var harnessAllowlist = Slice(
Harness,
"const readOnlyAllowedOutboundMessageTypes",
"const fullUiDbAllowedOutboundMessageTypes");
Assert.Contains("\"move-selected-playlist-rows\"", harnessAllowlist,
StringComparison.Ordinal);
Assert.Contains("\"set-playlist-enabled\"", harnessAllowlist,
StringComparison.Ordinal);
var runnerAllowlist = Slice(
Runner,
"$allowedOutbound = @(",
"if ($HarnessProfile -ceq 'DryRunPlayout')");
Assert.Contains("'move-selected-playlist-rows'", runnerAllowlist,
StringComparison.Ordinal);
Assert.Contains("'set-playlist-enabled'", runnerAllowlist,
StringComparison.Ordinal);
}
[Fact]
public void DryRunSealPinsProgramEnableAndShortcutEvidenceExactly()
{
var seal = Slice(
Runner,
"$playoutIntentContractOk =",
"$screenshotPathOk =");
foreach (var token in new[]
{
"programEnableParity.currentEntryId",
"programEnableParity.skippedEntryId",
"programEnableParity.nextEntryId",
"programEnableParity.setAllIntentCount -eq 2",
"programEnableParity.toggleActiveIntentCount -eq 1",
"programEnableParity.endOfPlaylistObserved -eq $true",
"programDragParity.appendIntentCount -eq 1",
"programDragParity.cleanupDeleteIntentCount -eq 1",
"programDragParity.onAirEntryPreserved -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 DryRunPhysicallyCancelsF2PickerAndFailClosesMissingF3Default()
{
var nativeInput = Slice(
Runner,
"public static void SendBackgroundFunctionKey(",
"public static void SendEscapeToForegroundWindow");
Assert.Contains("key != VirtualKeyF2 && key != VirtualKeyF3", nativeInput,
StringComparison.Ordinal);
Assert.Contains("AssertNoForbiddenTcp(processId, forbiddenPort)", nativeInput,
StringComparison.Ordinal);
Assert.Contains("allowForegroundTransitionAfterKey", nativeInput,
StringComparison.Ordinal);
var fixture = Slice(
Runner,
"function Initialize-BackgroundShortcutFixture",
"function Assert-NoUnsafeEnvironment");
Assert.Contains("$Profile -cne 'DryRunPlayout'", fixture,
StringComparison.Ordinal);
Assert.Contains("$ExpectedInstallLocation", fixture,
StringComparison.Ordinal);
Assert.Contains("[IO.FileAttributes]::ReparsePoint", fixture,
StringComparison.Ordinal);
Assert.Contains("The F3 fail-closed test requires the external default background asset to be absent.",
fixture, StringComparison.Ordinal);
Assert.Contains("@(Get-ChildItem -LiteralPath $root -Force).Count -ne 0", fixture,
StringComparison.Ordinal);
Assert.Contains("[IO.Directory]::Delete($root, $false)", fixture,
StringComparison.Ordinal);
var picker = Slice(
Runner,
"function Get-BackgroundPickerWindowHandles",
"function Invoke-ExactWindowsInputRequest");
foreach (var token in new[]
{
"WindowClassName($handle) -cne '#32770'",
"$BackgroundPickerTitle",
"WindowProcessId($handle) -ne $Application.Id",
"OwnerChainContains(",
"The exact app-owned background picker is not modal.",
"SendEscapeToForegroundWindow($picker.Handle)",
"The exact background picker was not destroyed after the one Escape input.",
"The external default background appeared; the one F3 fail-closed input was not sent."
})
{
Assert.Contains(token, picker, StringComparison.Ordinal);
}
var harness = Slice(
Harness,
"async function requestPhysicalBackgroundShortcuts()",
"async function installProbe()");
AssertInOrder(
harness,
"operation: \"background-f2-picker-cancel\"",
"[\"choose-background\"]",
"operation: \"background-f3-none\"",
"snapshot.state.statusKind === \"error\"",
"[\"toggle-background\"]",
"defaultAssetAbsentFailClosed: true");
Assert.Contains("return 3", Runner, StringComparison.Ordinal);
Assert.Contains("backgroundShortcutParity.f2KeyCalls -eq 1", Runner,
StringComparison.Ordinal);
Assert.Contains("backgroundShortcutParity.f3KeyCalls -eq 1", Runner,
StringComparison.Ordinal);
Assert.Contains("backgroundShortcutParity.finalBackgroundEnabled -eq $false", Runner,
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(
"namedPlaylistRecovery = [bool]$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 captured-pointer safe-tail drag",
"PROGRAM captured-pointer tail append",
"delete PROGRAM drag-appended future tail",
"PROGRAM drag-appended tail cleanup",
"PROGRAM safe-tail double-click append",
"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);
Assert.Contains("programDragParity =", sequence,
StringComparison.Ordinal);
Assert.Contains("onAirEntryPreserved: true", sequence,
StringComparison.Ordinal);
}
[Fact]
public void ProgramNamedPlaylistProfilePhysicallyCreatesSavesReadsAndDeletesWhileOnAir()
{
Assert.Contains("'ProgramPlaylistDb'", Runner, StringComparison.Ordinal);
Assert.Contains("'program-playlist-db'", Runner, StringComparison.Ordinal);
Assert.Contains("\"program-playlist-db\"", Harness, StringComparison.Ordinal);
var profile = Slice(
Harness,
"const programPlaylistDbProfile",
"const hardDeadlineEpochMilliseconds");
AssertInOrder(
profile,
"playout-background-file",
"playout-background-none",
"playout-prepare",
"playout-next",
"db-load-button");
foreach (var allowed in new[]
{
"create-named-playlist",
"save-current-named-playlist-to",
"delete-selected-named-playlist",
"dismiss-dialog",
"take-in",
"take-out"
})
{
Assert.Contains($"\"{allowed}\"", profile, StringComparison.Ordinal);
}
var sequence = Slice(
Harness,
"async function executeProgramNamedPlaylistCrud",
"const manualFinancialScreens");
AssertInOrder(
sequence,
"enter DryRun PROGRAM for named-playlist persistence",
"PROGRAM open named-playlist save dialog",
"PROGRAM create named-playlist definition",
"PROGRAM confirm named-playlist row save",
"PROGRAM reopen save dialog for fresh readback",
"PROGRAM named-playlist fresh readback",
"PROGRAM confirm named-playlist delete",
"PROGRAM named-playlist delete and absence",
"leave DryRun PROGRAM after named-playlist CRUD",
"PROGRAM named-playlist final IDLE");
Assert.DoesNotContain(
"document.getElementById(\"db-load-button\")",
sequence,
StringComparison.Ordinal);
var safeSequence = Slice(
Harness,
"async function executeSafeSequence",
"async function executeDryRunPlayoutSequence");
Assert.Contains("if (!programPlaylistDbProfile)", safeSequence,
StringComparison.Ordinal);
Assert.Contains("document.getElementById(\"db-load-button\")", safeSequence,
StringComparison.Ordinal);
Assert.Contains("writes.length !== 3", sequence, StringComparison.Ordinal);
Assert.Contains("\"저장되었습니다\"", sequence, StringComparison.Ordinal);
Assert.DoesNotContain("?€?λ릺?덉뒿?덈떎", sequence,
StringComparison.Ordinal);
Assert.Contains("definitionId: readback.definitionId", sequence,
StringComparison.Ordinal);
Assert.Contains("saveDefinitionId: created.definitionId", sequence,
StringComparison.Ordinal);
Assert.Contains(
"snapshot.state.namedPlaylist.documentFreshness === \"fresh\"",
sequence,
StringComparison.Ordinal);
Assert.Contains(
"snapshot.state.namedPlaylist.listFreshness === \"fresh\"",
sequence,
StringComparison.Ordinal);
Assert.Contains(
"snapshot.state.namedPlaylist.documentFreshness === \"none\"",
sequence,
StringComparison.Ordinal);
Assert.Contains("snapshot.state.namedPlaylist.rowCount === 0", sequence,
StringComparison.Ordinal);
Assert.Contains("rowCount: savedRowCount", sequence,
StringComparison.Ordinal);
Assert.Contains("freshDefinitionReadbackVerified: true", sequence,
StringComparison.Ordinal);
Assert.Contains("exactAbsenceVerified: true", sequence,
StringComparison.Ordinal);
Assert.Contains("beforeProgramDeleteWriteCount", sequence,
StringComparison.Ordinal);
Assert.Contains("waitForNamedPlaylistDeleteDispatch(", sequence,
StringComparison.Ordinal);
var finalIdle = Slice(
sequence,
"PROGRAM named-playlist final IDLE",
"restore deletion selection after PROGRAM named-playlist CRUD");
Assert.Contains("allowActiveDryRun: true", finalIdle,
StringComparison.Ordinal);
var deleteDispatch = Slice(
Harness,
"async function waitForNamedPlaylistDeleteDispatch",
"function assertNamedPlaylistRecoverySafe");
Assert.Contains("Date.now() + 5_000", deleteDispatch,
StringComparison.Ordinal);
Assert.Contains("writeCount === 1", deleteDispatch,
StringComparison.Ordinal);
Assert.Contains("produced no delete intent", deleteDispatch,
StringComparison.Ordinal);
Assert.Contains("failKnown", deleteDispatch, StringComparison.Ordinal);
Assert.Contains("failUnknown", deleteDispatch, StringComparison.Ordinal);
var seal = Slice(
Runner,
"elseif ($HarnessProfile -ceq 'ProgramPlaylistDb')",
"$playoutIntentContractOk =");
foreach (var token in new[]
{
"approvedDatabaseWriteMessages).Count -eq 3",
"create-named-playlist",
"save-current-named-playlist-to",
"delete-selected-named-playlist",
"saveDefinitionId",
"definitionId",
"requestId",
"freshDefinitionReadbackVerified -eq $true",
"exactAbsenceVerified -eq $true",
"finalPhase -ceq 'idle'"
})
{
Assert.Contains(token, seal, 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 ReadOnlyPhysicalSequenceDirectlyClicksPlaylistButtonsAndIndividualCheckbox()
{
var sequence = Slice(
Harness,
"async function executeSafeSequence",
"async function executeDryRunPlayoutSequence");
AssertInOrder(
sequence,
"move selected playlist row with Up button",
"playlist Up button reorder",
"restore selected playlist row with Down button",
"playlist Down button reorder",
"disable playlist row with its checkbox",
"individual playlist checkbox disabled row",
"restore playlist row with its checkbox",
"individual playlist checkbox restored row",
"stage direct selected-delete button row",
"select direct button delete fixture",
"delete selected playlist row with button",
"selected-delete button removed exact fixture");
Assert.Contains(
"document.getElementById(\"playlist-move-up\")",
sequence,
StringComparison.Ordinal);
Assert.Contains(
"document.getElementById(\"playlist-move-down\")",
sequence,
StringComparison.Ordinal);
Assert.Contains(
"document.getElementById(\"playlist-delete-selected\")",
sequence,
StringComparison.Ordinal);
Assert.Contains(
"input[type=\"checkbox\"]",
sequence,
StringComparison.Ordinal);
}
[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.");
}
}