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