565 lines
23 KiB
C#
565 lines
23 KiB
C#
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.");
|
|
}
|
|
}
|