fix: complete legacy operator parity workflows
This commit is contained in:
@@ -188,8 +188,15 @@ test("named playlist save requests the native saved dialog then closes on exact
|
||||
assertOrdered(confirmationOpen, [
|
||||
"rememberModalOpener(namedPlaylistConfirmationDialog)",
|
||||
"namedPlaylistConfirmationModal.hidden = false",
|
||||
"namedPlaylistConfirmationYes.disabled = false",
|
||||
"namedPlaylistConfirmationNo.disabled = false",
|
||||
"syncModalFocus()"
|
||||
]);
|
||||
const confirmationLock = section(
|
||||
"namedPlaylistConfirmationYes.disabled = pendingNamedPlaylistCommand",
|
||||
"if (deferredSelectionAction)");
|
||||
assert.doesNotMatch(confirmationLock, /operatorMutationLocked|uiBusy/,
|
||||
"an unrelated PROGRAM refresh must not cancel a physical confirmation click");
|
||||
assertOrdered(resolution, [
|
||||
"namedPlaylistConfirmationModal.hidden = true",
|
||||
"syncModalFocus()",
|
||||
|
||||
@@ -83,6 +83,31 @@ public sealed class LegacyStockCutInteractionParityTests
|
||||
row => Assert.Equal(LegacyStockCutCatalog.Rows[1].RawLabel, row.RawCutLabel));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DropPreservesExistingPrefixAndAppendsSelectedCutsAtTheTail()
|
||||
{
|
||||
var controller = new LegacyOperatorController(new QueryLookup());
|
||||
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
|
||||
controller.SelectStock(0);
|
||||
|
||||
controller.CutPointerDown(0, false, false, 2, 1, 1_000);
|
||||
var initial = controller.CutPointerDown(0, false, false, 2, 1, 1_100);
|
||||
var existing = Assert.Single(initial.Playlist);
|
||||
|
||||
controller.CutPointerDown(1, false, false, 2, 24, 2_000);
|
||||
controller.CutPointerDown(2, true, false, 2, 47, 2_100);
|
||||
var dropped = controller.DropSelectedCutsOnPlaylist();
|
||||
|
||||
Assert.Equal(3, dropped.Playlist.Count);
|
||||
Assert.Equal(existing, dropped.Playlist[0]);
|
||||
Assert.Equal(
|
||||
[
|
||||
LegacyStockCutCatalog.Rows[1].RawLabel,
|
||||
LegacyStockCutCatalog.Rows[2].RawLabel
|
||||
],
|
||||
dropped.Playlist.Skip(1).Select(row => row.RawCutLabel));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmptySearchButtonPreservesResultsStockAndCutSelection()
|
||||
{
|
||||
|
||||
@@ -99,6 +99,7 @@ public sealed class LegacyNativeDialogParityWebTests
|
||||
foreach (var label in new[]
|
||||
{
|
||||
"PList confirm row save",
|
||||
"PROGRAM confirm named-playlist row save",
|
||||
"GraphE ${screen} save",
|
||||
"GraphE ${screen} delete",
|
||||
"GraphE ${screen} delete-all cancel branch",
|
||||
@@ -110,7 +111,7 @@ public sealed class LegacyNativeDialogParityWebTests
|
||||
}
|
||||
|
||||
Assert.Equal(
|
||||
7,
|
||||
8,
|
||||
PackageInputHarness.Split(
|
||||
"pointerClickWithNativeDialog(",
|
||||
StringSplitOptions.None).Length - 1);
|
||||
|
||||
@@ -24,6 +24,55 @@ public sealed class LegacyPackageInputSmokeContractTests
|
||||
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()
|
||||
{
|
||||
@@ -56,13 +105,35 @@ public sealed class LegacyPackageInputSmokeContractTests
|
||||
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 =",
|
||||
"if ([int]$evidence.schemaVersion");
|
||||
"$screenshotPathOk =");
|
||||
foreach (var token in new[]
|
||||
{
|
||||
"programEnableParity.currentEntryId",
|
||||
@@ -71,6 +142,9 @@ public sealed class LegacyPackageInputSmokeContractTests
|
||||
"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'",
|
||||
@@ -91,6 +165,78 @@ public sealed class LegacyPackageInputSmokeContractTests
|
||||
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()
|
||||
{
|
||||
@@ -126,7 +272,9 @@ public sealed class LegacyPackageInputSmokeContractTests
|
||||
"$recoveryEvidence.writeCount -eq 1",
|
||||
"@('committedFresh', 'committedOptimistic')",
|
||||
"$namedPlaylistDeleteWriteCount -eq 2");
|
||||
Assert.Contains("-not $namedPlaylistRecoveryContractOk", Runner,
|
||||
Assert.Contains(
|
||||
"namedPlaylistRecovery = [bool]$namedPlaylistRecoveryContractOk",
|
||||
Runner,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
@@ -354,6 +502,11 @@ public sealed class LegacyPackageInputSmokeContractTests
|
||||
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\"",
|
||||
@@ -377,6 +530,143 @@ public sealed class LegacyPackageInputSmokeContractTests
|
||||
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]
|
||||
@@ -460,6 +750,46 @@ public sealed class LegacyPackageInputSmokeContractTests
|
||||
"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()
|
||||
{
|
||||
|
||||
@@ -118,7 +118,7 @@ public sealed class LegacyPlayoutNativeContractTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LocalSelectionAndDoubleClickGesturesQueueBehindRefreshInArrivalOrder()
|
||||
public void LocalSelectionDoubleClickAndCutDropQueueBehindRefreshInArrivalOrder()
|
||||
{
|
||||
var process = Normalize(Slice(
|
||||
Window,
|
||||
@@ -144,13 +144,13 @@ public sealed class LegacyPlayoutNativeContractTests
|
||||
Assert.Contains("LegacyActivateOperatorCatalogStockIntent", classifier,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyCutPointerDownIntent", classifier, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyDropSelectedCutsIntent", 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);
|
||||
}
|
||||
@@ -474,8 +474,20 @@ public sealed class LegacyPlayoutNativeContractTests
|
||||
Assert.Contains("CanDeleteSelectedPlaylistTail(status, workflow)", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("isSafeCutTailAppend", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("CanAppendCutToPlaylistTail(status, workflow)", Playout,
|
||||
StringComparison.Ordinal);
|
||||
var safeCutTailClassifier = Slice(
|
||||
Playout,
|
||||
"var isSafeCutTailAppend",
|
||||
"// MainForm kept its Space");
|
||||
Assert.Contains("LegacyCutPointerDownIntent { AllowAppend: true }",
|
||||
safeCutTailClassifier, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyDropSelectedCutsIntent", safeCutTailClassifier,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("isSafeProgramEnabledStateMutation", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("isSafeProgramTailDelete", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("refreshAllowsMutation", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("!status.IsPlayCompletionPending", Playout, StringComparison.Ordinal);
|
||||
Assert.Contains("!status.IsTakeOutCompletionPending", Playout, StringComparison.Ordinal);
|
||||
@@ -539,6 +551,8 @@ public sealed class LegacyPlayoutNativeContractTests
|
||||
"private static bool IsFixedSectionBatchMutationIntent");
|
||||
Assert.Contains("LegacyCutPointerDownIntent { AllowAppend: true }", mutationClassifier,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyDropSelectedCutsIntent", mutationClassifier,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacyCutKeyDownIntent", mutationClassifier,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacyClearCutSelectionIntent", mutationClassifier,
|
||||
@@ -554,6 +568,47 @@ public sealed class LegacyPlayoutNativeContractTests
|
||||
"private static bool IsOrderedPlayoutIntent");
|
||||
Assert.Contains("allowAppend: false", selectionOnly,
|
||||
StringComparison.Ordinal);
|
||||
var queuedLocalIntents = Slice(
|
||||
Window,
|
||||
"private static bool IsQueuedLocalSelectionIntent",
|
||||
"private static bool IsOperatorSettingsIntent");
|
||||
Assert.Contains("LegacyDropSelectedCutsIntent", queuedLocalIntents,
|
||||
StringComparison.Ordinal);
|
||||
var safeTailAdmission = Slice(
|
||||
Playout,
|
||||
"private bool CanAppendCutToPlaylistTail(",
|
||||
"private static bool RequiresIdlePlaylistMutation");
|
||||
Assert.Contains("status.OnAirSceneName", safeTailAdmission,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("workflow.OnAirCutCode", safeTailAdmission,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("workflow.CurrentEntryId", safeTailAdmission,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("workflow.CurrentCueIndexZeroBased", safeTailAdmission,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("currentIndex < playlist.Count", safeTailAdmission,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("playlist.Count(row => string.Equals(", safeTailAdmission,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var orderedTailDelete = Slice(
|
||||
Window,
|
||||
"else if (IsOrderedPlaylistTailMutationIntent(intent))",
|
||||
"else if (IsQueuedLocalSelectionIntent(intent))");
|
||||
Assert.Contains("_orderedPlaylistTailMutationIntentGate.WaitAsync(",
|
||||
orderedTailDelete, StringComparison.Ordinal);
|
||||
Assert.Contains("0,", orderedTailDelete, StringComparison.Ordinal);
|
||||
Assert.Contains("_orderedLocalSelectionIntentGate.WaitAsync(",
|
||||
orderedTailDelete, StringComparison.Ordinal);
|
||||
Assert.Contains("await _intentGate.WaitAsync", orderedTailDelete,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"intent is LegacyDeleteSelectedPlaylistRowsIntent",
|
||||
Window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_orderedPlaylistTailMutationIntentGate.Release();",
|
||||
Window,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static void AssertKnownFailureDoesNotQuarantine(
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyProgramNamedPlaylistPersistenceContractTests
|
||||
{
|
||||
private static readonly string Playout = File.ReadAllText(Path.Combine(
|
||||
FindRepositoryRoot(),
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"MainWindow.Playout.cs"));
|
||||
|
||||
private static readonly string Window = File.ReadAllText(Path.Combine(
|
||||
FindRepositoryRoot(),
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"MainWindow.xaml.cs"));
|
||||
|
||||
private static readonly string Web = File.ReadAllText(Path.Combine(
|
||||
FindRepositoryRoot(),
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web",
|
||||
"app.js"));
|
||||
|
||||
[Fact]
|
||||
public void ProgramAllowsNamedPlaylistWritesWithoutOpeningDbLoadReplacement()
|
||||
{
|
||||
var admission = Slice(
|
||||
Playout,
|
||||
"private bool CanAcceptOperatorMutation(",
|
||||
"private static bool RequiresIdlePlaylistMutation");
|
||||
var safeWrites = Slice(
|
||||
admission,
|
||||
"var isSafeNamedPlaylistPersistenceMutation",
|
||||
"var refreshAllowsMutation");
|
||||
|
||||
Assert.Contains("LegacyCreateNamedPlaylistIntent", safeWrites,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacySaveCurrentNamedPlaylistIntent", safeWrites,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacySaveCurrentNamedPlaylistToIntent", safeWrites,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyDeleteSelectedNamedPlaylistIntent", safeWrites,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacyRefreshNamedPlaylistsIntent", safeWrites,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacyLoadSelectedNamedPlaylistIntent", safeWrites,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacyLoadNamedPlaylistByIdIntent", safeWrites,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("isSafeNamedPlaylistPersistenceMutation ||", admission,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var idleOnly = Slice(
|
||||
Playout,
|
||||
"private static bool RequiresIdlePlaylistMutation",
|
||||
"private static bool IsPlaylistMutationIdle");
|
||||
Assert.Contains("LegacyLoadSelectedNamedPlaylistIntent", idleOnly,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyLoadNamedPlaylistByIdIntent", idleOnly,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacyCreateNamedPlaylistIntent", idleOnly,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacySaveCurrentNamedPlaylistIntent", idleOnly,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacySaveCurrentNamedPlaylistToIntent", idleOnly,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacyDeleteSelectedNamedPlaylistIntent", idleOnly,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NamedPlaylistModalRequestsUseOnePendingSlotBehindRefresh()
|
||||
{
|
||||
Assert.Contains(
|
||||
"private readonly SemaphoreSlim _orderedNamedPlaylistIntentGate = new(1, 1);",
|
||||
Window,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var process = Slice(
|
||||
Window,
|
||||
"private async Task ProcessIntentAsync",
|
||||
"private LegacyOperatorSnapshot SelectCutWithoutAppend");
|
||||
var queued = Slice(
|
||||
process,
|
||||
"else if (IsNamedPlaylistModalIntent(intent))",
|
||||
"// Keep the zero-timeout admission used by database and edit intents;");
|
||||
Assert.Contains("_orderedNamedPlaylistIntentGate.WaitAsync(", queued,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("0,", queued, StringComparison.Ordinal);
|
||||
Assert.Contains("await _intentGate.WaitAsync", queued,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_orderedNamedPlaylistIntentGate.Release();", process,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var classifier = Slice(
|
||||
Window,
|
||||
"private static bool IsNamedPlaylistModalIntent",
|
||||
"private LegacyOperatorSnapshot ReportIntentFailure");
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"LegacyRefreshNamedPlaylistsIntent",
|
||||
"LegacySelectNamedPlaylistIntent",
|
||||
"LegacyLoadSelectedNamedPlaylistIntent",
|
||||
"LegacyLoadNamedPlaylistByIdIntent",
|
||||
"LegacyCreateNamedPlaylistIntent",
|
||||
"LegacySaveCurrentNamedPlaylistIntent",
|
||||
"LegacySaveCurrentNamedPlaylistToIntent",
|
||||
"LegacyDeleteSelectedNamedPlaylistIntent",
|
||||
"LegacyDismissDialogIntent"
|
||||
})
|
||||
{
|
||||
Assert.Contains(intent, classifier, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("else if (intent is LegacyDismissDialogIntent)", process,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("PostState(_controller.Current);", process,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OneButtonResultDialogCannotLoseItsClickToBackgroundBusyState()
|
||||
{
|
||||
Assert.Contains(
|
||||
"dialogOk.disabled = legacyDialogIsConfirmation && state.isBusy === true;",
|
||||
Web,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NamedPlaylistConfirmationReenablesBothActionsWhenItOpens()
|
||||
{
|
||||
var open = Slice(
|
||||
Web,
|
||||
"function openNamedPlaylistConfirmation",
|
||||
"function restoreNamedPlaylistAfterConfirmation");
|
||||
|
||||
Assert.Contains("if (!options || isNamedPlaylistActionLocked()) return false;",
|
||||
open,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("namedPlaylistConfirmationYes.disabled = false;",
|
||||
open,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("namedPlaylistConfirmationNo.disabled = false;",
|
||||
open,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenNamedPlaylistConfirmationDoesNotLoseClickToProgramRefresh()
|
||||
{
|
||||
var render = Slice(
|
||||
Web,
|
||||
"function renderNamedPlaylist",
|
||||
"function clearOperatorCatalogDragVisuals");
|
||||
var confirmationLock = Slice(
|
||||
render,
|
||||
"namedPlaylistConfirmationYes.disabled =",
|
||||
"if (deferredSelectionAction)");
|
||||
|
||||
Assert.Contains("pendingNamedPlaylistCommand !== null", confirmationLock,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("namedPlaylistState.isWriteInProgress === true", confirmationLock,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("operatorMutationLocked", confirmationLock,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("uiBusy", confirmationLock,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string Slice(string source, string startMarker, string endMarker)
|
||||
{
|
||||
var start = source.IndexOf(startMarker, StringComparison.Ordinal);
|
||||
var end = source.IndexOf(endMarker, start + startMarker.Length,
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(start >= 0, $"Missing source marker: {startMarker}");
|
||||
Assert.True(end > start, $"Missing source marker: {endMarker}");
|
||||
return source[start..end];
|
||||
}
|
||||
|
||||
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("Could not locate the repository root.");
|
||||
}
|
||||
}
|
||||
@@ -216,6 +216,17 @@ if (($actualRoutes -join ',') -cne ($expectedRoutes -join ',')) {
|
||||
throw 'The 30-route validation order changed.'
|
||||
}
|
||||
|
||||
$probeFixedIdBlock = [regex]::Match(
|
||||
$stage,
|
||||
'const fixedIds = new Set\(\[(?<body>.*?)\]\);',
|
||||
[Text.RegularExpressions.RegexOptions]::Singleline)
|
||||
if (-not $probeFixedIdBlock.Success) { throw 'The probe fixed-action allowlist is missing.' }
|
||||
foreach ($actionId in @('fixed-072', 'fixed-073', 'fixed-074', 'fixed-075')) {
|
||||
if (-not $probeFixedIdBlock.Groups['body'].Value.Contains("'$actionId'")) {
|
||||
throw "The S6001 physical action is not probe-allowlisted: $actionId"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($token in @(
|
||||
'S6001_ASSET_INDEPENDENT_ACTIONS',
|
||||
"actionId: 'fixed-072'",
|
||||
@@ -226,6 +237,12 @@ foreach ($token in @(
|
||||
"subtype: 'WtiCrude'",
|
||||
"subtype: 'BrentCrude'",
|
||||
"subtype: 'Gold'",
|
||||
"physicalSelectCatalogTab(client, 'overseas')",
|
||||
'#market-tabs button[data-tab-id=',
|
||||
'current.probe.nativeStateCount > nativeStateBefore',
|
||||
"current.dom.activeWorkspace === 'catalog'",
|
||||
'current.dom.catalogVisible === true',
|
||||
"types[0] !== 'select-tab'",
|
||||
'physicalDoubleClickAndWait(client, selector',
|
||||
"current.state.currentCode === '6001'")) {
|
||||
if (-not $stage.Contains($token)) {
|
||||
|
||||
Reference in New Issue
Block a user